diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95ba81a..296ec2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,6 @@ jobs: - name: Get dependencies run: go mod download - name: Build - run: go build -v cmd/oidc-cli.go + run: go build -v -o oidc-cli cmd/** - name: Run tests run: go test -v ./... \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 98d7cab..2ede0d8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -17,7 +17,7 @@ builds: - linux - windows - darwin - main: ./cmd/oidc-cli.go + main: ./cmd/ binary: oidc-cli archives: diff --git a/README.md b/README.md index c133161..95097dc 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,21 @@ Command-line OIDC client, get a token without all the fuss ## Usage ```bash -Usage: oidc-cli [flags] - setup an openid client with the callback url : http://localhost:9555/callback and set below flags to get a token response +oidc-cli is a command-line OIDC client, get a token without all the fuss + +Usage: + oidc-cli [flags] [command-flags] + +Commands: + authorization_code: Uses the authorization code flow to get a token response + client_credentials: Uses the client credentials flow to get a token response + help : Prints help + Flags: - --discovery-url OpenID discovery endpoint. If provided, authorization-url and token-url will be ignored. - --authorization-url authorization URL. Default value is https://localhost:9443/oauth2/authorize. - --token-url token URL. Default value is https://localhost:9443/oauth2/token - --client-id client ID. - --client-secret client secret. - --scopes scopes. + +Run `oidc-cli -h` to get help for a specific command ``` + ## Installing Installing with homebrew @@ -27,13 +32,12 @@ You can also download a suitable release for your platform from the [releases pa ## Run ```bash -go run cmd/oidc-cli.go --authorization-url --token-url --client-id --client-secret +go run cmd/** authorization_code --authorization-url --token-url --client-id --client-secret --scopes "openid profile" ``` - ## Build ```bash -go build cmd/oidc-cli.go + go build -v -o oidc-cli ./cmd/** ``` diff --git a/authorization_code.go b/authorization_code.go new file mode 100644 index 0000000..eeab46d --- /dev/null +++ b/authorization_code.go @@ -0,0 +1,15 @@ +package oidc + +type AuthorizationCodeConfig struct { + DiscoveryEndpoint string + AuthorizationEndpoint string + TokenEndpoint string + ClientID string + ClientSecret string + Scopes string +} + +func (c *AuthorizationCodeConfig) Run() error { + HandleOpenIDFlow(c.ClientID, c.ClientSecret, c.Scopes, "http://localhost:9555/callback", c.DiscoveryEndpoint, c.AuthorizationEndpoint, c.TokenEndpoint) + return nil +} diff --git a/cmd/authorization_code_cfg.go b/cmd/authorization_code_cfg.go new file mode 100644 index 0000000..71aaafe --- /dev/null +++ b/cmd/authorization_code_cfg.go @@ -0,0 +1,27 @@ +package main + +import ( + "bytes" + "flag" + oidc "github.com/jentz/vigilant-dollop" +) + +func parseAuthorizationCodeFlags(name string, args []string) (config oidc.Command, output string, err error) { + flags := flag.NewFlagSet(name, flag.ContinueOnError) + var buf bytes.Buffer + flags.SetOutput(&buf) + + var conf oidc.AuthorizationCodeConfig + flags.StringVar(&conf.DiscoveryEndpoint, "discovery-url", "", "set OIDC discovery url") + flags.StringVar(&conf.AuthorizationEndpoint, "authorization-url", "", "set OIDC authorization url") + flags.StringVar(&conf.TokenEndpoint, "token-url", "", "set OIDC token url") + flags.StringVar(&conf.ClientID, "client-id", "", "set client ID") + flags.StringVar(&conf.ClientSecret, "client-secret", "", "set client secret") + flags.StringVar(&conf.Scopes, "scopes", "", "set scopes as a space separated list") + err = flags.Parse(args) + if err != nil { + return nil, buf.String(), err + } + + return &conf, buf.String(), nil +} diff --git a/cmd/oidc-cli.go b/cmd/oidc-cli.go index aea1f77..caf0b83 100644 --- a/cmd/oidc-cli.go +++ b/cmd/oidc-cli.go @@ -1,40 +1,92 @@ package main import ( + "errors" "flag" "fmt" - "log" - oidc "github.com/jentz/vigilant-dollop" + "os" + "slices" ) -func main() { +type Command struct { + Name string + Help string + Configure func(name string, args []string) (config oidc.Command, output string, err error) +} + +var commands = []Command{ + {Name: "authorization_code", Help: "Uses the authorization code flow to get a token response", Configure: parseAuthorizationCodeFlags}, + {Name: "client_credentials", Help: "Uses the client credentials flow to get a token response"}, + {Name: "help", Help: "Prints help"}, +} + +func usage() { + intro := `oidc-cli is a command-line OIDC client, get a token without all the fuss - const callbackURL = "http://localhost:9555/callback" - flag.Usage = func() { - fmt.Println("Usage: oidc-cli\n" + - " setup an openid client with the callback url : " + callbackURL + " and set below flags to get a token response\n" + - "Flags:\n" + - " --discovery-url OpenID discovery endpoint. If provided, authorization-url and token-url will be ignored.\n" + - " --authorization-url authorization URL. Default value is https://localhost:9443/oauth2/authorize.\n" + - " --token-url token URL. Default value is https://localhost:9443/oauth2/token\n" + - " --client-id client ID.\n" + - " --client-secret client secret.\n" + - " --scopes scopes.") +Usage: + oidc-cli [flags] [command-flags]` + + fmt.Fprintln(os.Stderr, intro) + fmt.Fprintln(os.Stderr, "\nCommands:") + for _, cmd := range commands { + fmt.Fprintf(os.Stderr, " %-18s: %s\n", cmd.Name, cmd.Help) } - var discoveryEndpoint = flag.String("discovery-url", "", "OpenID discovery endpoint") - var authorizationEndpoint = flag.String("authorization-url", "https://localhost:9443/oauth2/authorize", "OAuth2 authorization URL") - var tokenEndpoint = flag.String("token-url", "https://localhost:9443/oauth2/token", "OAuth2 token URL") - var clientID = flag.String("client-id", "client", "OAuth2 client ID") - var clientSecret = flag.String("client-secret", "clientSecret", "OAuth2 client secret") - var scopes = flag.String("scopes", "openid", "OAuth2 scopes") + fmt.Fprintln(os.Stderr, "\nFlags:") + // Prints a help string for each flag we defined earlier using + // flag.BoolVar (and related functions) + flag.PrintDefaults() + + fmt.Fprintln(os.Stderr) + fmt.Fprintf(os.Stderr, "Run `oidc-cli -h` to get help for a specific command\n\n") +} + +func runCommand(name string, args []string) { + + cmdIdx := slices.IndexFunc(commands, func(cmd Command) bool { + return cmd.Name == name + }) + if cmdIdx < 0 { + fmt.Fprintf(os.Stderr, "command \"%s\" not found\n\n", name) + flag.Usage() + os.Exit(1) + } + + cmd := commands[cmdIdx] + if cmd.Name == "help" { + flag.Usage() + os.Exit(0) + } + + command, output, err := cmd.Configure(name, args) + if errors.Is(err, flag.ErrHelp) { + fmt.Println(output) + os.Exit(2) + } else if err != nil { + fmt.Println("got error:", err) + fmt.Println("output:\n", output) + os.Exit(1) + } + + if err := command.Run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v", err.Error()) + os.Exit(1) + } +} + +func main() { + flag.Usage = usage flag.Parse() - if *clientID == "" { - log.Fatal("client-id is required to run this command") - } else if *clientSecret == "" { - log.Fatal("client-secret is required to run this command") + + // If no command is specified, print usage and exit + if flag.NArg() < 1 { + usage() + os.Exit(1) } - oidc.HandleOpenIDFlow(*clientID, *clientSecret, *scopes, callbackURL, *discoveryEndpoint, *authorizationEndpoint, *tokenEndpoint) + + subCmd := flag.Arg(0) + subCmdArgs := flag.Args()[1:] + runCommand(subCmd, subCmdArgs) } diff --git a/go.mod b/go.mod index 88503a5..d128ca3 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,3 @@ module github.com/jentz/vigilant-dollop go 1.21 - diff --git a/oidc.go b/oidc.go index 19c97a3..c0e264d 100644 --- a/oidc.go +++ b/oidc.go @@ -14,6 +14,10 @@ import ( "time" ) +type Command interface { + Run() error +} + type callbackEndpoint struct { server *http.Server code string