Refactored and reorganized code

This commit is contained in:
David Allen 2024-02-23 16:06:07 -07:00
parent 86f8784c19
commit fdb0db389c
8 changed files with 384 additions and 127 deletions

View file

@ -1,7 +1,9 @@
package cmd package cmd
import ( import (
"davidallendj/oidc-auth/internal/util" "davidallendj/opal/internal/oauth"
"davidallendj/opal/internal/oidc"
"davidallendj/opal/internal/util"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -11,35 +13,51 @@ import (
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
type Config struct { type Server struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
RedirectUri []string `yaml:"redirect-uri"` }
type AuthEndpoints struct {
Identities string `yaml:"identities"`
TrustedIssuers string `yaml:"trusted-issuers"`
AccessToken string `yaml:"access-token"`
ServerConfig string `yaml:"server-config"`
}
type Config struct {
Server Server `yaml:"server"`
Client oauth.Client `yaml:"client"`
IdentityProvider oidc.IdentityProvider `yaml:"oidc"`
State string `yaml:"state"` State string `yaml:"state"`
ResponseType string `yaml:"response-type"` ResponseType string `yaml:"response-type"`
Scope []string `yaml:"scope"` Scope []string `yaml:"scope"`
ClientId string `yaml:"client.id"` AuthEndpoints AuthEndpoints `yaml:"urls"`
ClientSecret string `yaml:"client.secret"` OpenBrowser bool `yaml:"open-browser"`
OIDCHost string `yaml:"oidc.host"`
OIDCPort int `yaml:"oidc.port"`
IdentitiesUrl string `yaml:"identities-url"`
AccessTokenUrl string `yaml:"access-token-url"`
} }
func NewConfig() Config { func NewConfig() Config {
return Config{ return Config{
Server: Server{
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 3333, Port: 3333,
RedirectUri: []string{""}, },
Client: oauth.Client{
Id: "",
Secret: "",
RedirectUris: []string{""},
},
IdentityProvider: *oidc.NewIdentityProvider(),
State: util.RandomString(20), State: util.RandomString(20),
ResponseType: "code", ResponseType: "code",
Scope: []string{"openid", "profile", "email"}, Scope: []string{"openid", "profile", "email"},
ClientId: "", AuthEndpoints: AuthEndpoints{
ClientSecret: "", Identities: "",
OIDCHost: "127.0.0.1", AccessToken: "",
OIDCPort: 80, TrustedIssuers: "",
IdentitiesUrl: "", ServerConfig: "",
AccessTokenUrl: "", },
OpenBrowser: false,
} }
} }

View file

@ -1,9 +1,9 @@
package cmd package cmd
import ( import (
"davidallendj/oidc-auth/internal/api" "davidallendj/opal/internal/api"
"davidallendj/oidc-auth/internal/oidc" "davidallendj/opal/internal/oidc"
"davidallendj/oidc-auth/internal/util" "davidallendj/opal/internal/util"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -13,83 +13,123 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( func hasRequiredParams(config *Config) bool {
identitiesUrl = "" return config.Client.Id != "" && config.Client.Secret != ""
accessTokenUrl = "" }
)
var loginCmd = &cobra.Command{ var loginCmd = &cobra.Command{
Use: "login", Use: "login",
Short: "Start the login flow", Short: "Start the login flow",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// load config if found
if configPath != "" { if configPath != "" {
exists, err := util.PathExists(configPath)
if err != nil {
fmt.Printf("failed to load config")
os.Exit(1)
} else if exists {
config = LoadConfig(configPath) config = LoadConfig(configPath)
} else { } else {
config = NewConfig() config = NewConfig()
} }
oidcProvider := oidc.NewOIDCProvider() }
oidcProvider.Host = config.OIDCHost // try and fetch server configuration if provided URL
oidcProvider.Port = config.OIDCPort idp := oidc.NewIdentityProvider()
if config.AuthEndpoints.ServerConfig != "" {
idp.FetchServerConfig(config.AuthEndpoints.ServerConfig)
} else {
// otherwise, use what's provided in config file
idp.Issuer = config.IdentityProvider.Issuer
idp.Endpoints = config.IdentityProvider.Endpoints
idp.Supported = config.IdentityProvider.Supported
}
// check if the client ID is set // check if all appropriate parameters are set in config
if config.ClientId == "" { if !hasRequiredParams(&config) {
fmt.Printf("client ID must be set\n") fmt.Printf("client ID must be set\n")
os.Exit(1) os.Exit(1)
} }
// build the authorization URL to redirect user for social sign-in
var authorizationUrl = util.BuildAuthorizationUrl( var authorizationUrl = util.BuildAuthorizationUrl(
oidcProvider.GetAuthorizeUrl(), idp.Endpoints.Authorize,
config.ClientId, config.Client.Id,
config.RedirectUri, config.Client.RedirectUris,
config.State, config.State,
config.ResponseType, config.ResponseType,
config.Scope, config.Scope,
) )
// print the authorization URL for the user to log in // print the authorization URL for sharing
fmt.Printf("login with identity provider: %s\n", authorizationUrl) serverAddr := fmt.Sprintf("%s:%d", config.IdentityProvider.Issuer)
fmt.Printf(`Login with identity provider:
%s/login
%s\n`,
serverAddr, authorizationUrl,
)
// automatically open browser to initiate login flow (only useful for testing)
if config.OpenBrowser {
util.OpenUrl(authorizationUrl)
}
// authorize oauth client and listen for callback from provider // authorize oauth client and listen for callback from provider
fmt.Printf("waiting for response from OIDC provider...\n") fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", serverAddr)
code, err := api.WaitForAuthorizationCode(config.Host, config.Port) code, err := api.WaitForAuthorizationCode(serverAddr, authorizationUrl)
if errors.Is(err, http.ErrServerClosed) { if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("server closed\n") fmt.Printf("Server closed.\n")
} else if err != nil { } else if err != nil {
fmt.Printf("error starting server: %s\n", err) fmt.Printf("Error starting server: %s\n", err)
os.Exit(1) os.Exit(1)
} }
// use code from response and exchange for bearer token // use code from response and exchange for bearer token (with ID token)
tokenString, err := api.FetchToken(code, oidcProvider.GetTokenUrl(), config.ClientId, config.ClientSecret, config.State, config.RedirectUri) tokenString, err := api.FetchIssuerToken(
code,
idp.Endpoints.Token,
config.Client,
config.State,
)
if err != nil { if err != nil {
fmt.Printf("%v\n", err) fmt.Printf("%v\n", err)
return return
} }
// extract ID token from bearer as JSON string for easy consumption
var data map[string]any var data map[string]any
json.Unmarshal([]byte(tokenString), &data) json.Unmarshal([]byte(tokenString), &data)
idToken := data["id_token"].(string) idToken := data["id_token"].(string)
// create a new identity with Ory Kratos if identitiesUrl is provided // create a new identity with identity and session manager if url is provided
if config.IdentitiesUrl != "" { if config.AuthEndpoints.Identities != "" {
api.CreateIdentity(config.IdentitiesUrl, idToken) api.CreateIdentity(config.AuthEndpoints.Identities, idToken)
api.FetchIdentities(config.IdentitiesUrl) api.FetchIdentities(config.AuthEndpoints.Identities)
} }
// use ID token/user info to get access token from Ory Hydra // fetch JWKS and add issuer to authentication server to submit ID token
if config.AccessTokenUrl != "" { jwk, err := api.FetchJwk("")
api.FetchAccessToken(config.AccessTokenUrl, config.ClientId, idToken) if err != nil {
fmt.Printf("failed to fetch JWK: %v\n", err)
} else {
api.AddTrustedIssuer(config.AuthEndpoints.TrustedIssuers, jwk.(string))
}
// use ID token/user info to fetch access token from authentication server
if config.AuthEndpoints.AccessToken != "" {
api.FetchAccessToken(config.AuthEndpoints.AccessToken, config.Client.Id, idToken, config.Scope)
} }
}, },
} }
func init() { func init() {
loginCmd.Flags().StringVar(&config.ClientId, "client.id", config.ClientId, "set the client ID") loginCmd.Flags().StringVar(&config.Client.Id, "client.id", config.Client.Id, "set the client ID")
loginCmd.Flags().StringVar(&config.ClientSecret, "client.secret", config.ClientSecret, "set the client secret") loginCmd.Flags().StringVar(&config.Client.Secret, "client.secret", config.Client.Secret, "set the client secret")
loginCmd.Flags().StringSliceVar(&config.RedirectUri, "redirect-uri", config.RedirectUri, "set the redirect URI") loginCmd.Flags().StringSliceVar(&config.Client.RedirectUris, "redirect-uri", config.Client.RedirectUris, "set the redirect URI")
loginCmd.Flags().StringVar(&config.ResponseType, "response-type", config.ResponseType, "set the response-type") loginCmd.Flags().StringVar(&config.ResponseType, "response-type", config.ResponseType, "set the response-type")
loginCmd.Flags().StringSliceVar(&config.Scope, "scope", config.Scope, "set the scopes") loginCmd.Flags().StringSliceVar(&config.Scope, "scope", config.Scope, "set the scopes")
loginCmd.Flags().StringVar(&config.State, "state", config.State, "set the state") loginCmd.Flags().StringVar(&config.State, "state", config.State, "set the state")
loginCmd.Flags().StringVar(&config.Host, "host", config.Host, "set the listening host") loginCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the listening host")
loginCmd.Flags().IntVar(&config.Port, "port", config.Port, "set the listening port") loginCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the listening port")
loginCmd.Flags().BoolVar(&config.OpenBrowser, "open-browser", config.OpenBrowser, "automatically open link in browser")
rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(loginCmd)
} }

12
go.mod
View file

@ -1,13 +1,23 @@
module davidallendj/oidc-auth module davidallendj/opal
go 1.22.0 go 1.22.0
require ( require (
github.com/lestrrat-go/jwx v1.2.28
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.17.0 // indirect
) )

74
go.sum
View file

@ -1,13 +1,87 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA=
github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -2,40 +2,65 @@ package api
import ( import (
"bytes" "bytes"
"davidallendj/opal/internal/oauth"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
) )
func WaitForAuthorizationCode(host string, port int) (string, error) { func WaitForAuthorizationCode(serverAddr string, loginUrl string) (string, error) {
var code string var code string
s := &http.Server{ s := &http.Server{Addr: serverAddr}
Addr: fmt.Sprintf("%s:%d", host, port), http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
} // redirect directly to identity provider with this endpoint
http.Redirect(w, r, loginUrl, http.StatusSeeOther)
})
http.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/oidc/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")
fmt.Printf("authorization code: %v\n", code) fmt.Printf("Authorization code: %v\n", code)
} }
s.Close() s.Close()
}) })
fmt.Printf("listening for authorization code at %s/oidc/callback\n", s.Addr)
return code, s.ListenAndServe() return code, s.ListenAndServe()
} }
func FetchToken(code string, remoteUrl string, clientId string, clientSecret string, state string, redirectUri []string) (string, error) { func FetchIssuerToken(code string, remoteUrl string, client oauth.Client, state string) (string, error) {
var token string var token string
data := url.Values{ data := url.Values{
"grant_type": {"authorization_code"}, "grant_type": {"authorization_code"},
"code": {code}, "code": {code},
"client_id": {clientId}, "client_id": {client.Id},
"client_secret": {clientSecret}, "client_secret": {client.Secret},
"state": {state}, "state": {state},
"redirect_uri": {strings.Join(redirectUri, ",")}, "redirect_uri": {strings.Join(client.RedirectUris, ",")},
}
res, err := http.PostForm(remoteUrl, data)
if err != nil {
return "", fmt.Errorf("failed to get ID token: %s", err)
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %v", err)
}
token = string(b)
fmt.Printf("%v\n", token)
return token, nil
}
func FetchAccessToken(remoteUrl string, clientId string, jwt string, scopes []string) (string, error) {
// hydra endpoint: /oauth/token
var token string
data := url.Values{
"grant_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
"assertion": {jwt},
} }
res, err := http.PostForm(remoteUrl, data) res, err := http.PostForm(remoteUrl, data)
if err != nil { if err != nil {
@ -53,31 +78,34 @@ func FetchToken(code string, remoteUrl string, clientId string, clientSecret str
return token, nil return token, nil
} }
func FetchAccessToken(remoteUrl string, clientId string, jwt string) (string, error) { func AddTrustedIssuer(remoteUrl string, issuer string, subject string, duration time.Duration, jwk string, scope []string) error {
var token string // hydra endpoint: /admin/trust/grants/jwt-bearer/issuers
data := url.Values{ data := []byte(fmt.Sprintf(`{
"grant_type": {"client_credentials"}, "allow_any_subject": false,
"client_id": {clientId}, "issuer": "%s",
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, "subject": "%s"
"client_assertion": {jwt}, "expires_at": "%v"
} "jwk": %v,
res, err := http.PostForm(remoteUrl, data) "scope": [ j%s ],
if err != nil { }`, issuer, subject, time.Now().Add(duration), jwk, strings.Join(scope, ",")))
return "", fmt.Errorf("failed to get token: %s", err)
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body) req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to read response body: %v", err) return fmt.Errorf("failed to create a new request: %v", err)
} }
token = string(b) req.Header.Add("Content-Type", "application/json")
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
fmt.Printf("%v\n", token) client := &http.Client{}
return token, nil res, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %v", err)
}
fmt.Printf("%d\n", res.StatusCode)
return nil
} }
func CreateIdentity(remoteUrl string, idToken string) error { func CreateIdentity(remoteUrl string, idToken string) error {
// kratos endpoint: /admin/identities
data := []byte(`{ data := []byte(`{
"schema_id": "preset://email", "schema_id": "preset://email",
"traits": { "traits": {
@ -114,3 +142,7 @@ func FetchIdentities(remoteUrl string) error {
fmt.Printf("%v\n", res) fmt.Printf("%v\n", res)
return nil return nil
} }
func RedirectSuccess() {
// show a success page with the user's access token
}

View file

@ -1,15 +1,15 @@
package oauth package oauth
type Client struct { type Client struct {
Id string Id string `yaml:"id"`
Secret string Secret string `yaml:"secret"`
Issuer string RedirectUris []string `yaml:"redirect_uris"`
} }
func NewClient() *Client { func NewClient() *Client {
return &Client{ return &Client{
Id: "", Id: "",
Secret: "", Secret: "",
Issuer: "", RedirectUris: []string{""},
} }
} }

View file

@ -1,38 +1,100 @@
package oidc package oidc
import "fmt" import (
"context"
"fmt"
type OpenIDConnectProvider struct { "github.com/lestrrat-go/jwx/jwk"
Host string )
Port int
AuthorizeEndpoint string type IdentityProvider struct {
TokenEndpoint string Issuer string `json:"issuer" yaml:"issuer"`
ConfigEndpoint string Endpoints Endpoints `json:"endpoints" yaml:"endpoints"`
Supported Supported `json:"supported" yaml:"supported"`
Key jwk.Key
} }
func NewOIDCProvider() *OpenIDConnectProvider { type Endpoints struct {
return &OpenIDConnectProvider{ Authorize string `json:"authorize_endpoint" yaml:"authorize"`
Host: "127.0.0.1", Token string `json:"token_endpoint" yaml:"token"`
Port: 80, Revocation string `json:"revocation_endpoint" yaml:"revocation"`
AuthorizeEndpoint: "/oauth/authorize", Introspection string `json:"introspection_endpoint" yaml:"introspection"`
TokenEndpoint: "/oauth/token", UserInfo string `json:"userinfo_endpoint" yaml:"userinfo"`
Jwks string `json:"jwks_uri" yaml:"jwks_uri"`
}
type Supported struct {
ResponseTypes []string `json:"response_types_supported"`
ResponseModes []string `json:"response_modes_supported"`
GrantTypes []string `json:"grant_types_supported"`
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"`
SubjectTypes []string `json:"subject_types_supported"`
IdTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported"`
ClaimTypes []string `json:"claim_types_supported"`
Claims []string `json:"claims_supported"`
}
func NewIdentityProvider() *IdentityProvider {
p := &IdentityProvider{Issuer: "127.0.0.1"}
p.Endpoints = Endpoints{
Authorize: p.Issuer + "/oauth/authorize",
Token: p.Issuer + "/oauth/token",
Revocation: p.Issuer + "/oauth/revocation",
Introspection: p.Issuer + "/oauth/introspect",
UserInfo: p.Issuer + "/oauth/userinfo",
Jwks: p.Issuer + "/oauth/discovery/keys",
} }
} p.Supported = Supported{
ResponseTypes: []string{"code"},
func (oidc *OpenIDConnectProvider) GetAuthorizeUrl() string { ResponseModes: []string{"query"},
if oidc.Port != 80 { GrantTypes: []string{
return fmt.Sprintf("%s:%d", oidc.Host, oidc.Port) + oidc.AuthorizeEndpoint "authorization_code",
"client_credentials",
"refresh_token",
},
TokenEndpointAuthMethods: []string{
"client_secret_basic",
"client_secret_post",
},
SubjectTypes: []string{"public"},
IdTokenSigningAlgValues: []string{"RS256"},
ClaimTypes: []string{"normal"},
Claims: []string{
"iss",
"sub",
"aud",
"exp",
"iat",
},
} }
return oidc.Host + oidc.AuthorizeEndpoint return p
} }
func (oidc *OpenIDConnectProvider) GetTokenUrl() string { func (p *IdentityProvider) FetchServerConfig(url string) {
if oidc.Port != 80 {
return fmt.Sprintf("%s:%d", oidc.Host, oidc.Port) + oidc.TokenEndpoint
}
return oidc.Host + oidc.TokenEndpoint
}
func (oidc *OpenIDConnectProvider) FetchServerConfiguration(url string) {
// make a request to a server's openid-configuration // make a request to a server's openid-configuration
} }
func (p *IdentityProvider) FetchJwk(url string) error {
//
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
set, err := jwk.Fetch(ctx, url)
if err != nil {
return fmt.Errorf("%v", err)
}
// get the first JWK from set
for it := set.Iterate(context.Background()); it.Next(context.Background()); {
pair := it.Pair()
p.Key = pair.Value.(jwk.Key)
return nil
}
return fmt.Errorf("failed to load public key: %v", err)
}
func (p *IdentityProvider) GetRawJwk() (any, error) {
var rawkey any
if err := p.Key.Raw(&rawkey); err != nil {
return nil, fmt.Errorf("failed to get raw key: %v", err)
}
return rawkey, nil
}

View file

@ -5,6 +5,8 @@ import (
"math/rand" "math/rand"
"net/url" "net/url"
"os" "os"
"os/exec"
"runtime"
"strings" "strings"
) )
@ -58,3 +60,22 @@ func PathExists(path string) (bool, error) {
} }
return false, err return false, err
} }
// https://stackoverflow.com/questions/39320371/how-start-web-server-to-open-page-in-browser-in-golang
// open opens the specified URL in the default browser of the user.
func OpenUrl(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}