diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..e1d8426 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,18 @@ +package cmd + +import "github.com/spf13/cobra" + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Create a new default config file", + Run: func(cmd *cobra.Command, args []string) { + // create a new config at all args (paths) + for _, path := range args { + _ = path + } + }, +} + +func init() { + rootCmd.AddCommand(configCmd) +} diff --git a/cmd/login b/cmd/login new file mode 100644 index 0000000..ac2d376 --- /dev/null +++ b/cmd/login @@ -0,0 +1,62 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + host string + port int + redirectUri = []string{""} + state = "" + responseType = "code" + scope = []string{"email", "profile", "openid"} + client oauth.Client +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Start the login flow" + Run: func(cmd *cobra.Command, args []string) { + oidcProvider := oidc.NewOIDCProvider() + var authorizationUrl = util.BuildAuthorizationUrl( + oidcProvider.GetAuthorizeUrl(), + client.Id, + redirectUri, + util.RandomString(20), + responseType, + []string{"email", "profile", "openid"}, + ) + + // print the authorization URL for the user to log in + fmt.Printf("Login with identity provider: %s\n", authorizationUrl) + + // start a HTTP server to listen for callback responses + fmt.Printf("Waiting for response from OIDC provider...\n") + err := server.Start(host, port) + if errors.Is(err, http.ErrServerClosed) { + fmt.Printf("server closed\n") + } else if err != nil { + fmt.Printf("error starting server: %s\n", err) + os.Exit(1) + } + + // extract code from response and exchange for bearer token + + // extract ID token and save user info + + // create a new identity with Ory Kratos + + // use ID token/user info to get access token from Ory Hydra + }, +} + + +func init(){ + loginCmd.Flags().StringVar(&client.Id, "client.id", "", "set the client ID") + loginCmd.Flags().StringVar(&redirectUri, "redirect-uri", "", "set the redirect URI") + loginCmd.Flags().StringVar(&responseType, "response-type", "code", "set the response-type") + loginCmd.Flags().StringSliceVar(&scope, "scope", []string{"openid", "email"}, "set the scopes") + loginCmd.Flags().String(&state, "state", util.RandomString(), "set the state") + loginCmd.Flags().StringVar(host, "host", "127.0.0.1", "set the listening host") + loginCmd.Flags().IntVar(&port, "port", 3333, "set the listening port") + rootCmd.AddCommand(loginCmd) +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..37c85fc --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var configPath = "" +var rootCmd = &cobra.Command{ + Use: "oidc", + Short: "An experimental OIDC helper tool for handling logins", + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err) + os.Exit(1) + } +} + +func init() { + rootCmd.Flags().StringVar(&configPath, "config", "", "set the config path") +} diff --git a/go.mod b/go.mod index 0892a8f..f823b7a 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,10 @@ -module davidallendj/ochami-auth +module davidallendj/oidc-auth -go 1.21.5 +go 1.22.0 -require github.com/ory/client-go v1.6.1 +require github.com/spf13/cobra v1.8.0 require ( - github.com/golang/protobuf v1.5.3 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index a1cac2d..d0e8c2c 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,10 @@ -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/ory/client-go v1.6.1 h1:nVb1ZRtBQS9oLJQR7VK7t9cxNHXKdZ2CtpoDmmHOhAQ= -github.com/ory/client-go v1.6.1/go.mod h1:6dx0Ir6q8O9mUvl3sqrlyR+0LalXLwwKedVDDmSPNQs= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler.go b/handler.go deleted file mode 100644 index 4459887..0000000 --- a/handler.go +++ /dev/null @@ -1,28 +0,0 @@ - -package main - -import ( - "encoding/json" - "html/template" - "net/http" -) - -func (app *App) dashboardHandler() http.HandlerFunc { - return func(writer http.ResponseWriter, request *http.Request) { - tmpl, err := template.New("index.html").ParseFiles("index.html") - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - return - } - session, err := json.Marshal(getSession(request.Context())) - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - return - } - err = tmpl.ExecuteTemplate(writer, "index.html", string(session)) - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - return - } - } -} diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go new file mode 100644 index 0000000..af578c5 --- /dev/null +++ b/internal/oauth/oauth.go @@ -0,0 +1,15 @@ +package oauth + +type Client struct { + Id string + Secret string + Issuer string +} + +func NewClient() *Client { + return &Client{ + Id: "", + Secret: "", + Issuer: "", + } +} diff --git a/internal/oidc/oidc-auth b/internal/oidc/oidc-auth new file mode 100755 index 0000000..f26efe6 Binary files /dev/null and b/internal/oidc/oidc-auth differ diff --git a/oidc/oidc.go b/internal/oidc/oidc.go similarity index 76% rename from oidc/oidc.go rename to internal/oidc/oidc.go index 6112ed8..6e15761 100644 --- a/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -7,7 +7,7 @@ type OpenIDConnectProvider struct { ConfigEndpoint string } -func NewOpenIDConnect() *OpenIDConnectProvider { +func NewOIDCProvider() *OpenIDConnectProvider { return &OpenIDConnectProvider{ Host: "https://gitlab.newmexicoconsortium.org", AuthorizeEndpoint: "/oauth/authorize", @@ -15,11 +15,11 @@ func NewOpenIDConnect() *OpenIDConnectProvider { } } -func (oidc *OpenIDConnectProvider) AuthorizeUrl() string { +func (oidc *OpenIDConnectProvider) GetAuthorizeUrl() string { return oidc.Host + oidc.AuthorizeEndpoint } -func (oidc *OpenIDConnectProvider) TokenUrl() string { +func (oidc *OpenIDConnectProvider) GetTokenUrl() string { return oidc.Host + oidc.TokenEndpoint } diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..62a1a8d --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,16 @@ +package server + +import ( + "fmt" + "net/http" +) + +func Start(host string, port int) error { + http.HandleFunc("/oauth/callback", getAuthorizationCode) + err := http.ListenAndServe(host+":"+fmt.Sprintf("%d", port), nil) + return err +} + +func getAuthorizationCode(w http.ResponseWriter, r *http.Request) { + fmt.Printf("response from OIDC provider: %v\n", r) +} diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..56a74e3 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,39 @@ +package util + +import ( + "math/rand" + "strings" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = rand.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} + +func BuildAuthorizationUrl(authEndpoint string, clientId string, redirectUri []string, state string, responseType string, scope []string) string { + return authEndpoint + "?" + "cilent_id=" + clientId + + "&redirect_url=" + strings.Join(redirectUri, ",") + + "&response_type=" + responseType + + "&state=" + state + + "&scope=" + strings.Join(scope, "+") +} diff --git a/main.go b/main.go index f99561d..ca4015a 100644 --- a/main.go +++ b/main.go @@ -1,35 +1,11 @@ package main -import ( - "davidallendj/ochami-auth/oidc" - "strings" -) +import "davidallendj/oidc-auth/cmd" var ( - clientId = "" - redirectUri = "" - state = "" - response_type = "code" - userDB = "" + userDB = "" ) -func buildAuthorizationUrl(authEndpoint string, clientId string, redirectUri []string, state string, responseType string, scope []string) string { - return authEndpoint + "?" + "cilent_id=" + clientId + - "&redirect_url=" + strings.Join(redirectUri, ",") + - "&response_type=" + responseType + - "&state=" + state + - "&scope=" + strings.Join(scope, "+") -} - - func main() { - client := oidc.NewOpenIDConnect() - var authorizationUrl = buildAuthorizationUrl( - client. - ) - var tokenUrl = loginHost + tokenEndpoint - // start a HTTP server to listen for callback responses - // extract code from response and exchange for bearer token - // extract ID token and save user info - // use ID token/user info to get access token from Hydra + cmd.Execute() }