Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

20 changed files with 223 additions and 731 deletions

View file

@ -4,7 +4,6 @@
name: Release with goreleaser name: Release with goreleaser
on: on:
workflow_dispatch:
push: push:
tags: tags:
- v* - v*
@ -53,3 +52,7 @@ jobs:
echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js
node process.js node process.js
echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT
- name: Attest opaal binary
uses: github-early-access/generate-build-provenance@main
with:
subject-path: opaal

View file

@ -23,7 +23,6 @@ dockers:
extra_files: extra_files:
- LICENSE.md - LICENSE.md
- README.md - README.md
- pages/
archives: archives:
- format: tar.gz - format: tar.gz
rlcp: true rlcp: true
@ -38,7 +37,6 @@ archives:
files: files:
- LICENSE.md - LICENSE.md
- README.md - README.md
- pages/
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:
@ -53,4 +51,4 @@ changelog:
# github: # github:
# name_template: "{{.Version}}" # name_template: "{{.Version}}"
# prerelease: auto # prerelease: auto
# mode: append # mode: append

View file

@ -2,7 +2,7 @@ FROM cgr.dev/chainguard/wolfi-base
RUN apk add --no-cache tini bash curl RUN apk add --no-cache tini bash curl
RUN mkdir -p /opaal/pages/static/stylesheets RUN mkdir /opaal
RUN chown 65534:65534 /opaal RUN chown 65534:65534 /opaal
WORKDIR /opaal WORKDIR /opaal
@ -10,8 +10,6 @@ WORKDIR /opaal
USER 65534:65534 USER 65534:65534
COPY opaal /opaal/opaal COPY opaal /opaal/opaal
COPY pages/* /opaal/pages/
CMD [ "/opaal/opaal" ] CMD [ "/opaal/opaal" ]

View file

@ -28,39 +28,12 @@ These commands will create a default config, then start the login process. Maybe
- [Gitlab](https://about.gitlab.com/) - [Gitlab](https://about.gitlab.com/)
- [Forgejo](https://forgejo.org/) (fork of Gitea) - [Forgejo](https://forgejo.org/) (fork of Gitea)
The tool is now able to run an internal example identity provider using the `serve` subcommand.
```bash
./opaal serve --config config.yaml
```
This will start a server that allows you to login with `opaal` itself. Currently, it is only has one example user to use for log in. The username and password combination is `ochami:ochami`. It uses the same config file as before with additional parameters set in the config file:
```yaml
server:
...
issuer:
host: "127.0.0.1"
port: 3332
authentication:
clients:
- id: "ochami"
secret: "ochami"
name: "ochami"
issuer: "http://127.0.0.1:3332"
redirect-uris:
- "http://127.0.0.1:3333/oidc/callback"
```
See the [Configuration](#configuration) section for the entire config file.
### Authorization Code Flow ### 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: `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) 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 - Alternatively, you can use a link produced
2. Login using identity provider credentials 2. Login using identity provider credentials
3. Authorize application registered with IdP 3. Authorize application registered with IdP
4. IdP redirects to specified redirect URI 4. IdP redirects to specified redirect URI
@ -68,29 +41,27 @@ See the [Configuration](#configuration) section for the entire config file.
- ...verifying the authenticity of the ID token from identity provider with its JWKS - ...verifying the authenticity of the ID token from identity provider with its JWKS
- ...adds itself as a trusted issuer to the authorization server with it's own JWK - ...adds itself as a trusted issuer to the authorization server with it's own JWK
- ...creates a new signed JWT to send to the authorization server with the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type - ...creates a new signed JWT to send to the authorization server with the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type
- ... returns an access token that can be used by services protected by the authorization server - ... returns an access token that can be used by services protected by the authorization server
*After receiving the ID token, the rest of the flow requires the appropriate URLs to be set to continue. *After receiving the ID token, the rest of the flow requires the appropriate URLs to be set to continue.
### Client Credentials Flow ### Client Credentials Flow
`opaal` also has
## Configuration ## Configuration
Here is an example configuration file: Here is an example configuration file:
```yaml ```yaml
version: "0.3.2" version: "0.0.1"
server: server:
host: "127.0.0.1" host: "127.0.0.1"
port: 3333 port: 3333
callback: "/oidc/callback" callback: "/oidc/callback"
issuer:
host: "127.0.0.1"
port: 3332
providers: providers:
opaal: "https://127.0.0.1:3332"
forgejo: "http://127.0.0.1:3000" forgejo: "http://127.0.0.1:3000"
authentication: authentication:
@ -112,17 +83,7 @@ authentication:
client-credentials: client-credentials:
authorization: authorization:
token: urls:
forwarding: false
refresh: false
duration: 16h
scope:
- smd.read
key-path: ./keys
endpoints:
issuer: http://127.0.0.1:4444
config: http://127.0.0.1:4444/.well-known/openid-configuration
jwks: http://127.0.0.1:4444/.well-known/jwks.json
#identities: http://127.0.0.1:4434/admin/identities #identities: http://127.0.0.1:4434/admin/identities
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
login: http://127.0.0.1:4433/self-service/login/api login: http://127.0.0.1:4433/self-service/login/api
@ -130,14 +91,17 @@ authorization:
authorize: http://127.0.0.1:4444/oauth2/auth authorize: http://127.0.0.1:4444/oauth2/auth
register: http://127.0.0.1:4444/oauth2/register register: http://127.0.0.1:4444/oauth2/register
token: http://127.0.0.1:4444/oauth2/token token: http://127.0.0.1:4444/oauth2/token
clients:
- id: bss
secret: IAMBSS
options: options:
decode-id-token: true
decode-access-token: true
run-once: true run-once: true
open-browser: false open-browser: false
flow: authorization_code forward: false
cache-only: false
verbose: true
``` ```
## Troubleshooting ## Troubleshooting

View file

@ -2,9 +2,12 @@ package cmd
import ( import (
opaal "davidallendj/opaal/internal" opaal "davidallendj/opaal/internal"
cache "davidallendj/opaal/internal/cache/sqlite"
"davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"fmt" "fmt"
"os" "os"
"slices"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -22,68 +25,68 @@ var loginCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
for { for {
// try and find client with valid identity provider config // try and find client with valid identity provider config
// var provider *oidc.IdentityProvider var provider *oidc.IdentityProvider
// if target != "" { if target != "" {
// // only try to use client with name give // only try to use client with name give
// index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool { index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
// return target == c.Name return target == c.Name
// }) })
// if index < 0 { if index < 0 {
// fmt.Printf("could not find the target client listed by name") fmt.Printf("could not find the target client listed by name")
// os.Exit(1) os.Exit(1)
// } }
// client := config.Authentication.Clients[index] client := config.Authentication.Clients[index]
// _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer) _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
// if err != nil { if err != nil {
// } }
// } else if targetIndex >= 0 { } else if targetIndex >= 0 {
// // only try to use client by index // only try to use client by index
// targetCount := len(config.Authentication.Clients) - 1 targetCount := len(config.Authentication.Clients) - 1
// if targetIndex > targetCount { if targetIndex > targetCount {
// fmt.Printf("target index out of range (found %d)", targetCount) fmt.Printf("target index out of range (found %d)", targetCount)
// } }
// client := config.Authentication.Clients[targetIndex] client := config.Authentication.Clients[targetIndex]
// _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer) _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
// if err != nil { if err != nil {
// } }
// } else { } else {
// for _, c := range config.Authentication.Clients { for _, c := range config.Authentication.Clients {
// // try to get identity provider info locally first // try to get identity provider info locally first
// _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer) _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer)
// if err != nil && !config.Options.CacheOnly { if err != nil && !config.Options.CacheOnly {
// fmt.Printf("fetching config from issuer: %v\n", c.Issuer) fmt.Printf("fetching config from issuer: %v\n", c.Issuer)
// // try to get info remotely by fetching // try to get info remotely by fetching
// provider, err = oidc.FetchServerConfig(c.Issuer) provider, err = oidc.FetchServerConfig(c.Issuer)
// if err != nil { if err != nil {
// fmt.Printf("failed to fetch server config: %v\n", err) fmt.Printf("failed to fetch server config: %v\n", err)
// continue continue
// } }
// client = c client = c
// // fetch the provider's JWKS // fetch the provider's JWKS
// err := provider.FetchJwks() err := provider.FetchJwks()
// if err != nil { if err != nil {
// fmt.Printf("failed to fetch JWKS: %v\n", err) fmt.Printf("failed to fetch JWKS: %v\n", err)
// } }
// break break
// } }
// // only test the first if --run-all flag is not set // only test the first if --run-all flag is not set
// if !config.Authentication.TestAllClients { if !config.Authentication.TestAllClients {
// fmt.Printf("stopping after first test...\n\n\n") fmt.Printf("stopping after first test...\n\n\n")
// break break
// } }
// } }
// } }
// if provider == nil { if provider == nil {
// fmt.Printf("failed to retrieve provider config\n") fmt.Printf("failed to retrieve provider config\n")
// os.Exit(1) os.Exit(1)
// } }
// start the listener // start the listener
err := opaal.Login(&config) err := opaal.Login(&config, &client, provider)
if err != nil { if err != nil {
fmt.Printf("%v\n", err) fmt.Printf("%v\n", err)
os.Exit(1) os.Exit(1)
@ -112,13 +115,3 @@ func init() {
loginCmd.MarkFlagsMutuallyExclusive("target.name", "target.index") loginCmd.MarkFlagsMutuallyExclusive("target.name", "target.index")
rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(loginCmd)
} }
func MakeButton(url string, text string) string {
// check if we have http:// a
html := "<input type=\"button\" "
html += "class=\"button\" "
html += fmt.Sprintf("onclick=\"window.location.href='%s';\" ", url)
html += fmt.Sprintf("value=\"%s\"", text)
return html
// return "<a href=\"" + url + "\"> " + text + "</a>"
}

View file

@ -1,40 +0,0 @@
package cmd
import (
opaal "davidallendj/opaal/internal"
"davidallendj/opaal/internal/oidc"
"errors"
"fmt"
"net/http"
"github.com/spf13/cobra"
)
var (
endpoints oidc.Endpoints
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start an simple, bare minimal identity provider server",
Long: "The built-in identity provider is not (nor meant to be) a complete OIDC implementation and behaves like an external IdP",
Run: func(cmd *cobra.Command, args []string) {
s := opaal.NewServerWithConfig(&config)
// FIXME: change how the server address is set with `NewServerWithConfig`
s.Server.Addr = fmt.Sprintf("%s:%d", s.Issuer.Host, s.Issuer.Port)
err := s.StartIdentityProvider()
if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("Identity provider server closed.\n")
} else if err != nil {
fmt.Printf("failed to start server: %v", err)
}
},
}
func init() {
serveCmd.Flags().StringVar(&endpoints.Authorization, "endpoints.authorization", "", "set the authorization endpoint for the identity provider")
serveCmd.Flags().StringVar(&endpoints.Token, "endpoints.token", "", "set the token endpoint for the identity provider")
serveCmd.Flags().StringVar(&endpoints.JwksUri, "endpoints.jwks_uri", "", "set the JWKS endpoints for the identity provider")
rootCmd.AddCommand(serveCmd)
}

12
go.mod
View file

@ -3,7 +3,7 @@ module davidallendj/opaal
go 1.22.0 go 1.22.0
require ( require (
github.com/davidallendj/go-utils v0.0.0-20240417195221-95765f3b9bad github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf
github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/chi/v5 v5.0.12
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
@ -11,12 +11,12 @@ require (
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/nikolalohinski/gonja/v2 v2.2.0 github.com/nikolalohinski/gonja/v2 v2.2.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
golang.org/x/net v0.24.0 golang.org/x/net v0.22.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/logr v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
@ -39,8 +39,8 @@ require (
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.22.0 // indirect golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
) )

28
go.sum
View file

@ -8,10 +8,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidallendj/go-utils v0.0.0-20240417195221-95765f3b9bad h1:WODRnqFS2CZfraXy7Nvh5qekM42/L5kvLoLMqNr50e8= github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf h1:gY89rDLnc+70S0JcyHoPGU+XFpMwY1iVYNQzdC/qAHc=
github.com/davidallendj/go-utils v0.0.0-20240417195221-95765f3b9bad/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs= github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
@ -96,20 +96,20 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 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= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 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-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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -2,7 +2,6 @@ package opaal
import ( import (
"davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -46,7 +45,6 @@ type TokenOptions struct {
Forwarding bool `yaml:"forwarding"` Forwarding bool `yaml:"forwarding"`
Refresh bool `yaml:"refresh"` Refresh bool `yaml:"refresh"`
Scope []string `yaml:"scope"` Scope []string `yaml:"scope"`
//TODO: allow specifying audience in returned token
} }
type Authentication struct { type Authentication struct {
@ -57,10 +55,9 @@ type Authentication struct {
} }
type Authorization struct { type Authorization struct {
Token TokenOptions `yaml:"token"`
Endpoints Endpoints `yaml:"endpoints"` Endpoints Endpoints `yaml:"endpoints"`
KeyPath string `yaml:"key-path"` KeyPath string `yaml:"key-path"`
Audience []string `yaml:"audience"` // NOTE: overrides the "aud" claim in token sent to authorization server Token TokenOptions `yaml:"token"`
} }
type Config struct { type Config struct {
@ -73,18 +70,11 @@ type Config struct {
} }
func NewConfig() Config { func NewConfig() Config {
config := Config{ return Config{
Version: goutil.GetCommit(), Version: goutil.GetCommit(),
Server: server.Server{ Server: server.Server{
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 3333, Port: 3333,
Issuer: server.IdentityProviderServer{
Endpoints: oidc.Endpoints{
Authorization: "",
Token: "",
JwksUri: "",
},
},
}, },
Options: Options{ Options: Options{
RunOnce: true, RunOnce: true,
@ -107,7 +97,6 @@ func NewConfig() Config {
}, },
}, },
} }
return config
} }
func LoadConfig(path string) Config { func LoadConfig(path string) Config {

View file

@ -4,6 +4,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@ -18,15 +19,14 @@ import (
) )
type JwtBearerFlowParams struct { type JwtBearerFlowParams struct {
AccessToken string AccessToken string
IdToken string IdToken string
// IdentityProvider *oidc.IdentityProvider IdentityProvider *oidc.IdentityProvider
TrustedIssuer *oauth.TrustedIssuer TrustedIssuer *oauth.TrustedIssuer
Client *oauth.Client Client *oauth.Client
Audience []string Refresh bool
Refresh bool Verbose bool
Verbose bool KeyPath string
KeyPath string
} }
type JwtBearerFlowEndpoints struct { type JwtBearerFlowEndpoints struct {
@ -39,36 +39,28 @@ type JwtBearerFlowEndpoints struct {
func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) { func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) {
// 1. verify that the JWT from the issuer is valid using all keys // 1. verify that the JWT from the issuer is valid using all keys
var ( var (
// idp = params.IdentityProvider idp = params.IdentityProvider
accessToken = params.AccessToken accessToken = params.AccessToken
idToken = params.IdToken idToken = params.IdToken
client = params.Client client = params.Client
trustedIssuer = params.TrustedIssuer trustedIssuer = params.TrustedIssuer
verbose = params.Verbose verbose = params.Verbose
) )
// pre-condition checks to make sure certain variables are set
if client == nil {
return "", fmt.Errorf("invalid client (client is nil)")
}
if verbose {
fmt.Printf("ID token (IDP): %s\n access token (IDP): %s", accessToken, idToken)
}
if accessToken != "" { if accessToken != "" {
_, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true)) _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to verify access token: %v", err) return "", fmt.Errorf("failed to verify access token: %v", err)
} }
} }
if idToken != "" { if idToken != "" {
_, err := jws.Verify([]byte(idToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true)) _, err := jws.Verify([]byte(idToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to verify ID token: %v", err) return "", fmt.Errorf("failed to verify ID token: %v", err)
} }
} }
// TODO: 2. Check if we are already registered as a trusted issuer with authorization server... // 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 // 3.a if not, create a new JWKS (or just JWK) to be verified
var ( var (
@ -85,7 +77,7 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
if err != nil { if err != nil {
return "", fmt.Errorf("failed to generate new RSA key: %v", err) return "", fmt.Errorf("failed to generate new RSA key: %v", err)
} }
privateJwk, publicJwk, err = GenerateJwkKeyPairFromPrivateKey(privateKey) // FIXME: needs to pull correct version from cryptox privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err) return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err)
} }
@ -134,24 +126,18 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
// TODO: add trusted issuer to cache if successful // TODO: add trusted issuer to cache if successful
// 4. create a new JWT based on the claims from the identity provider and sign // 4. create a new JWT based on the claims from the identity provider and sign
parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(client.Provider.KeySet)) parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(idp.KeySet))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse ID token: %v", err) return "", fmt.Errorf("failed to parse ID token: %v", err)
} }
payload := parsedIdToken.PrivateClaims() payload := parsedIdToken.PrivateClaims()
payload["iss"] = trustedIssuer.Issuer payload["iss"] = trustedIssuer.Issuer
payload["aud"] = []string{eps.Token} payload["aud"] = []string{eps.Token}
payload["iat"] = time.Now().Unix() payload["iat"] = time.Now().Unix()
payload["nbf"] = time.Now().Unix() payload["nbf"] = time.Now().Unix()
payload["exp"] = time.Now().Add(time.Second * 3600 * 16).Unix() payload["exp"] = time.Now().Add(time.Second * 3600).Unix()
payload["sub"] = "opaal" payload["sub"] = "opaal"
// if an "audience" value is set, then override the token endpoint value
if len(params.Audience) > 0 {
payload["aud"] = params.Audience
}
// include the offline_access scope if refresh tokens are enabled // include the offline_access scope if refresh tokens are enabled
if params.Refresh { if params.Refresh {
v, ok := payload["scope"] v, ok := payload["scope"]
@ -255,7 +241,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
var ( var (
client = params.Client client = params.Client
idToken = params.IdToken idToken = params.IdToken
// idp = params.IdentityProvider idp = params.IdentityProvider
verbose = params.Verbose verbose = params.Verbose
) )
@ -263,7 +249,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
if verbose { if verbose {
fmt.Printf("Fetching JWKS from authentication server for verification...\n") fmt.Printf("Fetching JWKS from authentication server for verification...\n")
} }
err := client.Provider.FetchJwks() err := idp.FetchJwks()
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch JWK: %v", err) return fmt.Errorf("failed to fetch JWK: %v", err)
} else { } else {
@ -273,7 +259,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
} }
ti := &oauth.TrustedIssuer{ ti := &oauth.TrustedIssuer{
Issuer: client.Provider.Issuer, Issuer: idp.Issuer,
Subject: "1", Subject: "1",
ExpiresAt: time.Now().Add(time.Second * 3600), ExpiresAt: time.Now().Add(time.Second * 3600),
} }
@ -353,15 +339,3 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
} }
return nil return nil
} }
func GenerateJwkKeyPairFromPrivateKey(privateKey *rsa.PrivateKey) (jwk.Key, jwk.Key, error) {
privateJwk, err := jwk.FromRaw(privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create private JWK: %v", err)
}
publicJwk, err := jwk.PublicKeyOf(privateJwk)
if err != nil {
return nil, nil, fmt.Errorf("failed to create public JWK: %v", err)
}
return privateJwk, publicJwk, nil
}

View file

@ -12,11 +12,19 @@ import (
"time" "time"
) )
func Login(config *Config) error { func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error {
if config == nil { if config == nil {
return fmt.Errorf("invalid config") return fmt.Errorf("invalid config")
} }
if client == nil {
return fmt.Errorf("invalid client")
}
if provider == nil {
return fmt.Errorf("invalid identity provider")
}
// make cache if it's not where expect // make cache if it's not where expect
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath) _, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
if err != nil { if err != nil {
@ -31,20 +39,25 @@ func Login(config *Config) error {
} }
// print the authorization URL for sharing // print the authorization URL for sharing
var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state)
s := NewServerWithConfig(config) s := NewServerWithConfig(config)
s.State = state fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
s.GetListenAddr(), authorizationUrl,
)
var button = MakeButton(authorizationUrl, "Login with "+client.Name)
var authzClient = oauth.NewClient() var authzClient = oauth.NewClient()
authzClient.Scope = config.Authorization.Token.Scope authzClient.Scope = config.Authorization.Token.Scope
// authorize oauth client and listen for callback from provider
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", s.GetListenAddr())
params := server.ServerParams{ params := server.ServerParams{
Verbose: config.Options.Verbose, Verbose: config.Options.Verbose,
AuthProvider: &oidc.IdentityProvider{ AuthProvider: &oidc.IdentityProvider{
Issuer: config.Authorization.Endpoints.Issuer, Issuer: config.Authorization.Endpoints.Issuer,
Endpoints: oidc.Endpoints{ Endpoints: oidc.Endpoints{
Config: config.Authorization.Endpoints.Config, Config: config.Authorization.Endpoints.Config,
Authorization: config.Authorization.Endpoints.Authorize, JwksUri: config.Authorization.Endpoints.JwksUri,
JwksUri: config.Authorization.Endpoints.JwksUri,
}, },
}, },
JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{ JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{
@ -53,7 +66,8 @@ func Login(config *Config) error {
Register: config.Authorization.Endpoints.Register, Register: config.Authorization.Endpoints.Register,
}, },
JwtBearerParams: flows.JwtBearerFlowParams{ JwtBearerParams: flows.JwtBearerFlowParams{
Client: authzClient, Client: authzClient,
IdentityProvider: provider,
TrustedIssuer: &oauth.TrustedIssuer{ TrustedIssuer: &oauth.TrustedIssuer{
AllowAnySubject: false, AllowAnySubject: false,
Issuer: s.Addr, Issuer: s.Addr,
@ -61,9 +75,8 @@ func Login(config *Config) error {
ExpiresAt: time.Now().Add(config.Authorization.Token.Duration), ExpiresAt: time.Now().Add(config.Authorization.Token.Duration),
Scope: []string{}, Scope: []string{},
}, },
Verbose: config.Options.Verbose, Verbose: config.Options.Verbose,
Refresh: config.Authorization.Token.Refresh, Refresh: config.Authorization.Token.Refresh,
Audience: config.Authorization.Audience,
}, },
ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{ ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{
Clients: config.Authorization.Endpoints.Clients, Clients: config.Authorization.Endpoints.Clients,
@ -74,7 +87,7 @@ func Login(config *Config) error {
Client: authzClient, Client: authzClient,
}, },
} }
err = s.StartLogin(config.Authentication.Clients, params) err = s.Start(button, provider, client, params)
if errors.Is(err, http.ErrServerClosed) { if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
} else if err != nil { } else if err != nil {
@ -83,7 +96,7 @@ func Login(config *Config) error {
} else if config.Options.FlowType == "client_credentials" { } else if config.Options.FlowType == "client_credentials" {
params := flows.ClientCredentialsFlowParams{ params := flows.ClientCredentialsFlowParams{
Client: nil, // # FIXME: need to do something about this being nil I think Client: client,
} }
_, err := NewClientCredentialsFlowWithConfig(config, params) _, err := NewClientCredentialsFlowWithConfig(config, params)
if err != nil { if err != nil {
@ -95,3 +108,12 @@ func Login(config *Config) error {
return nil return nil
} }
func MakeButton(url string, text string) string {
html := "<input type=\"button\" "
html += "class=\"button\" "
html += fmt.Sprintf("onclick=\"window.location.href='%s';\" ", url)
html += fmt.Sprintf("value=\"%s\"", text)
return html
// return "<a href=\"" + url + "\"> " + text + "</a>"
}

View file

@ -29,7 +29,7 @@ func NewClientWithConfig(config *Config) *oauth.Client {
Id: clients[0].Id, Id: clients[0].Id,
Secret: clients[0].Secret, Secret: clients[0].Secret,
Name: clients[0].Name, Name: clients[0].Name,
Provider: clients[0].Provider, Issuer: clients[0].Issuer,
Scope: clients[0].Scope, Scope: clients[0].Scope,
RedirectUris: clients[0].RedirectUris, RedirectUris: clients[0].RedirectUris,
} }
@ -53,7 +53,7 @@ func NewClientWithConfigByName(config *Config, name string) *oauth.Client {
func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client { func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client {
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool { index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
return c.Provider.Issuer == issuer return c.Issuer == issuer
}) })
if index >= 0 { if index >= 0 {
@ -90,12 +90,6 @@ func NewServerWithConfig(conf *Config) *server.Server {
}, },
Host: host, Host: host,
Port: port, Port: port,
Issuer: server.IdentityProviderServer{
Host: conf.Server.Issuer.Host,
Port: conf.Server.Issuer.Port,
Endpoints: conf.Server.Issuer.Endpoints,
Clients: conf.Server.Issuer.Clients,
},
} }
return server return server
} }

View file

@ -16,15 +16,13 @@ func (client *Client) IsFlowInitiated() bool {
return client.FlowId != "" return client.FlowId != ""
} }
func (client *Client) BuildAuthorizationUrl(state string) string { func (client *Client) BuildAuthorizationUrl(issuer string, state string) string {
url := client.Provider.Endpoints.Authorization + "?client_id=" + client.Id + return issuer + "?" + "client_id=" + client.Id +
"&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + "&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) +
"&response_type=code" + // this has to be set to "code" "&response_type=code" + // this has to be set to "code"
"&scope=" + strings.Join(client.Scope, "+") "&state=" + state +
if state != "" { "&scope=" + strings.Join(client.Scope, "+") +
url += "&state=" + state "&resource=" + url.QueryEscape("http://127.0.0.1:4444/oauth2/token")
}
return url
} }
func (client *Client) InitiateLoginFlow(loginUrl string) error { func (client *Client) InitiateLoginFlow(loginUrl string) error {
@ -93,7 +91,7 @@ func (client *Client) FetchCSRFToken(flowUrl string) error {
return fmt.Errorf("failed to extract CSRF token: not found") return fmt.Errorf("failed to extract CSRF token: not found")
} }
func (client *Client) FetchTokenFromAuthenticationServer(code string, state string) ([]byte, error) { func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
body := url.Values{ body := url.Values{
"grant_type": {"authorization_code"}, "grant_type": {"authorization_code"},
"client_id": {client.Id}, "client_id": {client.Id},
@ -107,16 +105,14 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, state stri
if state != "" { if state != "" {
body["state"] = []string{state} body["state"] = []string{state}
} }
res, err := http.PostForm(client.Provider.Endpoints.Token, body) res, err := http.PostForm(remoteUrl, body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get ID token: %v", err) return nil, fmt.Errorf("failed to get ID token: %s", err)
} }
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
fmt.Printf("%s\n", string(b))
defer res.Body.Close() defer res.Body.Close()
return b, nil // domain, _ := url.Parse("http://127.0.0.1")
// client.Jar.SetCookies(domain, res.Cookies())
return io.ReadAll(res.Body)
} }

View file

@ -1,7 +1,6 @@
package oauth package oauth
import ( import (
"davidallendj/opaal/internal/oidc"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -25,15 +24,15 @@ const (
type Client struct { type Client struct {
http.Client http.Client
Id string `db:"id" yaml:"id"` Id string `db:"id" yaml:"id"`
Secret string `db:"secret" yaml:"secret"` Secret string `db:"secret" yaml:"secret"`
Name string `db:"name" yaml:"name"` Name string `db:"name" yaml:"name"`
Description string `db:"description" yaml:"description"` Description string `db:"description" yaml:"description"`
Provider oidc.IdentityProvider `db:"issuer" yaml:"provider"` Issuer string `db:"issuer" yaml:"issuer"`
RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"` RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"`
RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"` RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"`
Scope []string `db:"scope" yaml:"scope"` Scope []string `db:"scope" yaml:"scope"`
Audience []string `db:"audience" yaml:"audience"` Audience []string `db:"audience" yaml:"audience"`
FlowId string FlowId string
CsrfToken string CsrfToken string
} }

View file

@ -111,11 +111,26 @@ func (p *IdentityProvider) LoadServerConfig(path string) error {
} }
func (p *IdentityProvider) FetchServerConfig() error { func (p *IdentityProvider) FetchServerConfig() error {
tmp, err := FetchServerConfig(p.Issuer) // make a request to a server's openid-configuration
req, err := http.NewRequest(http.MethodGet, p.Issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{}))
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create a new request: %v", err)
}
client := &http.Client{} // temp client to get info and not used in flow
res, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %v", err)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
}
err = p.ParseServerConfig(body)
if err != nil {
return fmt.Errorf("failed to parse server config: %v", err)
} }
p = tmp
return nil return nil
} }
@ -132,15 +147,10 @@ func FetchServerConfig(issuer string) (*IdentityProvider, error) {
return nil, fmt.Errorf("failed to do request: %v", err) return nil, fmt.Errorf("failed to do request: %v", err)
} }
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP status code: %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err) return nil, fmt.Errorf("failed to read response body: %v", err)
} }
var p IdentityProvider var p IdentityProvider
err = p.ParseServerConfig(body) err = p.ParseServerConfig(body)
if err != nil { if err != nil {
@ -164,25 +174,3 @@ func (p *IdentityProvider) FetchJwks() error {
return nil return nil
} }
func (p *IdentityProvider) UpdateEndpoints(other *IdentityProvider) {
UpdateEndpoints(&p.Endpoints, &other.Endpoints)
}
func UpdateEndpoints(eps *Endpoints, other *Endpoints) {
// only update endpoints that are not empty
var UpdateIfEmpty = func(ep *string, s string) {
if ep != nil {
if *ep == "" {
*ep = s
}
}
}
UpdateIfEmpty(&eps.Config, other.Config)
UpdateIfEmpty(&eps.Authorization, other.Authorization)
UpdateIfEmpty(&eps.Token, other.Token)
UpdateIfEmpty(&eps.Revocation, other.Revocation)
UpdateIfEmpty(&eps.Introspection, other.Introspection)
UpdateIfEmpty(&eps.UserInfo, other.UserInfo)
UpdateIfEmpty(&eps.JwksUri, other.JwksUri)
}

View file

@ -1,290 +0,0 @@
package server
import (
"crypto/rand"
"crypto/rsa"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/davidallendj/go-utils/cryptox"
"github.com/davidallendj/go-utils/util"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)
// TODO: make this a completely separate server
type IdentityProviderServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Endpoints oidc.Endpoints `yaml:"endpoints"`
Clients []RegisteredClient `yaml:"clients"`
}
// NOTE: could we use a oauth.Client here instead??
type RegisteredClient struct {
Id string `yaml:"id"`
Secret string `yaml:"secret"`
Name string `yaml:"name"`
RedirectUris []string `yaml:"redirect-uris"`
}
func (s *Server) StartIdentityProvider() error {
// NOTE: this example does NOT implement CSRF tokens nor use them
// create an example identity provider
var (
r = chi.NewRouter()
// clients = []oauth.Client{}
activeCodes = []string{}
)
// update endpoints that have values set
defaultEps := oidc.Endpoints{
Authorization: "http://" + s.Addr + "/oauth2/authorize",
Token: "http://" + s.Addr + "/oauth2/token",
JwksUri: "http://" + s.Addr + "/.well-known/jwks.json",
}
oidc.UpdateEndpoints(&s.Issuer.Endpoints, &defaultEps)
// generate key pair used to sign JWKS and create JWTs
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("failed to generate new RSA key: %v", err)
}
privateJwk, publicJwk, err := cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
if err != nil {
return fmt.Errorf("failed to generate JWK pair from private key: %v", err)
}
kid, _ := privateJwk.Get("kid")
publicJwk.Set("kid", kid)
publicJwk.Set("use", "sig")
publicJwk.Set("kty", "RSA")
publicJwk.Set("alg", "RS256")
if err := publicJwk.Validate(); err != nil {
return fmt.Errorf("failed to validate public JWK: %v", err)
}
// TODO: create .well-known JWKS endpoint with json
r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
// TODO: generate new JWKs from a private key
jwks := map[string]any{
"keys": []jwk.Key{
publicJwk,
},
}
b, err := json.Marshal(jwks)
if err != nil {
return
}
w.Write(b)
})
r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
// create config JSON to serve with GET request
config := map[string]any{
"issuer": "http://" + s.Addr,
"authorization_endpoint": s.Issuer.Endpoints.Authorization,
"token_endpoint": s.Issuer.Endpoints.Token,
"jwks_uri": s.Issuer.Endpoints.JwksUri,
"scopes_supported": []string{
"openid",
"profile",
"email",
},
"response_types_supported": []string{
"code",
},
"grant_types_supported": []string{
"authorization_code",
},
"id_token_signing_alg_values_supported": []string{
"RS256",
},
"claims_supported": []string{
"iss",
"sub",
"aud",
"exp",
"iat",
"name",
"email",
},
}
b, err := json.Marshal(config)
if err != nil {
return
}
w.Write(b)
})
r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
// serve up a simple login page
})
r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) {
// give consent for app to use
})
r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) {
// serve up a login page for user creds
form, err := os.ReadFile("pages/login.html")
if err != nil {
fmt.Printf("failed to load login form: %v", err)
}
w.Write(form)
})
r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
// check for example identity with POST request
r.ParseForm()
username := r.Form.Get("username")
password := r.Form.Get("password")
if len(s.Issuer.Clients) <= 0 {
fmt.Printf("no registered clients found with identity provider (add them in config)\n")
return
}
// example username and password so do simplified authorization code flow
if username == "openchami" && password == "openchami" {
client := s.Issuer.Clients[0]
// check if there are any redirect URIs supplied
if len(client.RedirectUris) <= 0 {
fmt.Printf("no redirect URIs found for client %s (ID: %s)\n", client.Name, client.Id)
return
}
for _, url := range client.RedirectUris {
// send an authorization code to each URI
code := util.RandomString(64)
activeCodes = append(activeCodes, code)
redirectUrl := fmt.Sprintf("%s?code=%s", url, code)
fmt.Printf("redirect URL: %s\n", redirectUrl)
http.Redirect(w, r, redirectUrl, http.StatusFound)
// _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil)
// if err != nil {
// fmt.Printf("failed to make request: %v\n", err)
// continue
// }
}
} else {
w.Write([]byte("error logging in"))
http.Redirect(w, r, "/browser/login", http.StatusUnauthorized)
}
})
r.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// check for authorization code and make sure it's valid
var code = r.Form.Get("code")
index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code })
if index < 0 {
fmt.Printf("invalid authorization code: %s\n", code)
return
}
// now create and return a JWT that can be verified with by authorization server
iat := time.Now().Unix()
exp := time.Now().Add(time.Second * 3600 * 16).Unix()
t := jwt.New()
t.Set(jwt.IssuerKey, s.Addr)
t.Set(jwt.SubjectKey, "ochami")
t.Set(jwt.AudienceKey, "ochami")
t.Set(jwt.IssuedAtKey, iat)
t.Set(jwt.ExpirationKey, exp)
t.Set("name", "ochami")
t.Set("email", "example@ochami.org")
t.Set("email_verified", true)
t.Set("scope", []string{
"openid",
"profile",
"email",
"example",
})
// payload := map[string]any{}
// payload["iss"] = s.Addr
// payload["aud"] = "ochami"
// payload["iat"] = iat
// payload["nbf"] = iat
// payload["exp"] = exp
// payload["sub"] = "ochami"
// payload["name"] = "ochami"
// payload["email"] = "example@ochami.org"
// payload["email_verified"] = true
// payload["scope"] = []string{
// "openid",
// "profile",
// "email",
// "example",
// }
payloadJson, err := json.MarshalIndent(t, "", "\t")
if err != nil {
fmt.Printf("failed to marshal payload: %v", err)
return
}
signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk))
if err != nil {
fmt.Printf("failed to sign token: %v\n", err)
return
}
// construct the bearer token with required fields
scope, _ := t.Get("scope")
bearer := map[string]any{
"token_type": "Bearer",
"id_token": string(signed),
"expires_in": exp,
"created_at": iat,
"scope": strings.Join(scope.([]string), " "),
}
b, err := json.MarshalIndent(bearer, "", "\t")
if err != nil {
fmt.Printf("failed to marshal bearer token: %v\n", err)
return
}
fmt.Printf("bearer: %s\n", string(b))
w.Write(b)
})
r.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
var (
responseType = r.URL.Query().Get("response_type")
clientId = r.URL.Query().Get("client_id")
redirectUris = r.URL.Query().Get("redirect_uri")
)
// check for required authorization code params
if responseType != "code" {
fmt.Printf("invalid response type\n")
return
}
// find a valid client
index := slices.IndexFunc(s.Issuer.Clients, func(c RegisteredClient) bool {
fmt.Printf("%s ? %s\n", c.Id, clientId)
return c.Id == clientId
})
if index < 0 {
fmt.Printf("no valid client found")
return
}
// TODO: check that our redirect URIs all match
for _, uri := range redirectUris {
_ = uri
}
// redirect to browser login since we don't do session management here
http.Redirect(w, r, "/browser/login", http.StatusFound)
})
s.Handler = r
return s.ListenAndServe()
}

View file

@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"slices"
"github.com/davidallendj/go-utils/httpx" "github.com/davidallendj/go-utils/httpx"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -18,11 +17,10 @@ import (
type Server struct { type Server struct {
*http.Server *http.Server
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
Callback string `yaml:"callback"` Callback string `yaml:"callback"`
State string `yaml:"state"` State string `yaml:"state"`
Issuer IdentityProviderServer `yaml:"issuer"`
} }
type ServerParams struct { type ServerParams struct {
@ -42,39 +40,12 @@ func (s *Server) GetListenAddr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port) return fmt.Sprintf("%s:%d", s.Host, s.Port)
} }
func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error {
var ( var target = ""
target string
callback string
client *oauth.Client
sso string
)
// check if callback is set // check if callback is set
if s.Callback == "" { if s.Callback == "" {
callback = "/oidc/callback" s.Callback = "/oidc/callback"
}
// make the login page SSO buttons and authorization URLs to write to stdout
buttons := ""
fmt.Printf("Login with an identity provider: \n")
for i, client := range clients {
// fetch provider configuration before adding button
p, err := oidc.FetchServerConfig(client.Provider.Issuer)
if err != nil {
fmt.Printf("failed to fetch server config: %v\n", err)
continue
}
// if we're able to get the config, go ahead and try to fetch jwks too
if err = p.FetchJwks(); err != nil {
fmt.Printf("failed to fetch JWKS: %v\n", err)
continue
}
clients[i].Provider = *p
buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name)
fmt.Printf("\t%s: /login?sso=%s\n", client.Name, client.Id)
} }
var code string var code string
@ -101,27 +72,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
// add target if query exists // add target if query exists
if r != nil { if r != nil {
target = r.URL.Query().Get("target") target = r.URL.Query().Get("target")
sso = r.URL.Query().Get("sso")
// TODO: get client from list and build the authorization URL string
index := slices.IndexFunc(clients, func(c oauth.Client) bool {
return c.Id == sso
})
// TODO: redirect the user to authorization URL and return from func
foundClient := index >= 0
if foundClient {
client = &clients[index]
url := client.BuildAuthorizationUrl(s.State)
if params.Verbose {
fmt.Printf("Redirect URL: %s\n", url)
}
http.Redirect(w, r, url, http.StatusFound)
return
}
} }
// show login page with notice to redirect // show login page with notice to redirect
template, err := gonja.FromFile("pages/index.html") template, err := gonja.FromFile("pages/index.html")
if err != nil { if err != nil {
@ -141,54 +92,47 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
p = params.AuthProvider p = params.AuthProvider
jwks []byte jwks []byte
) )
// try and get the JWKS from param first
fetchAndMarshal := func() (err error) { if p.Endpoints.JwksUri != "" {
err = p.FetchJwks() err := p.FetchJwks()
if err != nil { if err != nil {
fmt.Printf("failed to fetch keys: %v\n", err) fmt.Printf("failed to fetch keys using JWKS url...trying to fetch config and try again...\n")
return
} }
jwks, err = json.Marshal(p.KeySet) jwks, err = json.Marshal(p.KeySet)
if err != nil { if err != nil {
fmt.Printf("failed to marshal JWKS: %v\n", err) fmt.Printf("failed to marshal JWKS: %v\n", err)
} }
return } else if p.Endpoints.Config != "" && jwks == nil {
} // otherwise, try and fetch the whole config and try again
err := p.FetchServerConfig()
// try and get the JWKS from param first if err != nil {
if p.Endpoints.JwksUri != "" {
if err := fetchAndMarshal(); err != nil {
w.Write(jwks)
return
}
}
// otherwise or if fetching the JWKS failed, try and fetch the whole config first and try again
if p.Endpoints.Config != "" {
if err := p.FetchServerConfig(); err != nil {
fmt.Printf("failed to fetch server config: %v\n", err) fmt.Printf("failed to fetch server config: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Redirect(w, r, "/error", http.StatusInternalServerError)
return return
} }
} else { err = p.FetchJwks()
fmt.Printf("getting JWKS from param failed and endpoints config unavailable\n") if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err)
return http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
} }
if err := fetchAndMarshal(); err != nil { // forward the JWKS from the authorization server
fmt.Printf("failed to fetch and marshal JWKS after config update: %v\n", err) if jwks == nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) fmt.Printf("no JWKS was fetched from authorization server\n")
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return return
} }
w.Write(jwks) w.Write(jwks)
}) })
r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
// use refresh token provided to do a refresh token grant // use refresh token provided to do a refresh token grant
refreshToken := r.URL.Query().Get("refresh-token") refreshToken := r.URL.Query().Get("refresh-token")
if refreshToken != "" { if refreshToken != "" {
_, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(client.Provider.Endpoints.Token, refreshToken) _, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken)
if err != nil { if err != nil {
fmt.Printf("failed to perform refresh token grant: %v\n", err) fmt.Printf("failed to perform refresh token grant: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError) http.Redirect(w, r, "/error", http.StatusInternalServerError)
@ -207,8 +151,6 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
return return
} }
} else { } else {
// FIXME: I think this probably needs to reworked or removed
// NOTE: this logic fetches a token for services to retrieve like BSS
// perform a client credentials grant and return a token // perform a client credentials grant and return a token
var err error var err error
accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams) accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams)
@ -220,7 +162,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
w.Write([]byte(accessToken)) w.Write([]byte(accessToken))
} }
}) })
r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) { r.HandleFunc(s.Callback, func(w http.ResponseWriter, r *http.Request) {
// get the code from the OIDC provider // get the code from the OIDC provider
if r != nil { if r != nil {
code = r.URL.Query().Get("code") code = r.URL.Query().Get("code")
@ -228,15 +170,10 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
fmt.Printf("Authorization code: %v\n", code) fmt.Printf("Authorization code: %v\n", code)
} }
// make sure we have the correct client to use
if client == nil {
fmt.Printf("failed to find valid client")
return
}
// use code from response and exchange for bearer token (with ID token) // use code from response and exchange for bearer token (with ID token)
bearerToken, err := client.FetchTokenFromAuthenticationServer( bearerToken, err := client.FetchTokenFromAuthenticationServer(
code, code,
provider.Endpoints.Token,
s.State, s.State,
) )
if err != nil { if err != nil {
@ -266,7 +203,6 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
// complete JWT bearer flow to receive access token from authorization server // complete JWT bearer flow to receive access token from authorization server
// fmt.Printf("bearer: %v\n", string(bearerToken)) // fmt.Printf("bearer: %v\n", string(bearerToken))
params.JwtBearerParams.IdToken = data["id_token"].(string) params.JwtBearerParams.IdToken = data["id_token"].(string)
params.JwtBearerParams.Client = client
accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams) accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams)
if err != nil { if err != nil {
fmt.Printf("failed to complete JWT bearer flow: %v\n", err) fmt.Printf("failed to complete JWT bearer flow: %v\n", err)
@ -332,15 +268,3 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
s.Handler = r s.Handler = r
return s.ListenAndServe() return s.ListenAndServe()
} }
func makeButton(url string, text string) string {
// check if we have http:// a
// html := "<input type=\"button\" "
// html += fmt.Sprintf("onclick=\"window.location.href='%s';\" ", url)
// html += fmt.Sprintf("value=\"%s\">", text)
html := "<a "
html += "class=\"button\" "
html += fmt.Sprintf("href=\"%s\">%s", url, text)
html += "</a>"
return html
}

View file

@ -7,7 +7,6 @@
<div id="wrapper"> <div id="wrapper">
Log in using the option(s) below for an access token. </br></br> Log in using the option(s) below for an access token. </br></br>
{{loginForm}}
{{loginButtons}} {{loginButtons}}
</div> </div>
</html> </html>

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<div class="log-form">
<h2>Login to your account</h2>
<form action="/api/login" method="post">
<input type="text" id="username" name="username" title="username" placeholder="Enter your username or email..." /><br/>
<input type="password" id="password" name="password" title="password" placeholder="Enter your password..." /><br/>
<button type="submit" class="btn">Login</button><br/>
<a class="forgot" href="#">Forgot Username?</a><br/>
<label>(hint: try 'openchami' for both username and password)</label>
</form>
</div><!--end log form -->
</html>

View file

@ -27,10 +27,4 @@ input[type=text] {
padding: 12px 20px; padding: 12px 20px;
margin: 8px 0; margin: 8px 0;
box-sizing: border-box; box-sizing: border-box;
}
input[type=password] {
width: 80%;
padding: 12px 20px;
margin: 8px 0;
box-sizing: border-box;
} }