mirror of
https://github.com/davidallendj/opaal.git
synced 2026-02-04 08:46:27 -07:00
Compare commits
No commits in common. "main" and "v0.0.1" have entirely different histories.
20 changed files with 223 additions and 731 deletions
5
.github/workflows/build_release.yml
vendored
5
.github/workflows/build_release.yml
vendored
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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" ]
|
||||||
|
|
||||||
|
|
|
||||||
60
README.md
60
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
125
cmd/login.go
125
cmd/login.go
|
|
@ -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>"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
40
cmd/serve.go
40
cmd/serve.go
|
|
@ -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
12
go.mod
|
|
@ -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
28
go.sum
|
|
@ -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=
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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>"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue