Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 86 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

[![GoDoc][doc-img]][doc] [![Build Status][build-img]][build] [![codecov][codecov-img]][codecov] [![Go Report Card][goreport-img]][goreport]

`tokget` is a CLI tool that allows to get a user's access token and ID token by [the OpenID Connect protocol][oidc-spec-core].
`tokget` is a tool that allows getting a user's access token and ID token by [the OpenID Connect protocol][oidc-spec-core].

**Features**

- works as a CLI tool or a web server;
- authenticates a user without interaction between the browser and user;
- supports arbitrary structure of the login page;
- logs a user out by canceling an ID token.
Expand All @@ -31,18 +32,18 @@ go install ./...
From Docker:

```bash
docker pull icoreru/tokget:v1.1.0
docker pull icoreru/tokget:v1.2.0
```

Download binary:

```bash

curl -Lo /tmp/tokget_linux_amd64.tar.gz 'https://github.com/i-core/tokget/releases/download/v1.1.0/tokget_linux_amd64.tar.gz'
curl -Lo /tmp/tokget_linux_amd64.tar.gz 'https://github.com/i-core/tokget/releases/download/v1.2.0/tokget_linux_amd64.tar.gz'
tar -xzf /tmp/tokget_linux_amd64.tar.gz -C /usr/local/bin

# In alpine linux (as it does not come with curl by default)
wget -P /tmp 'https://github.com/i-core/tokget/releases/download/v1.1.0/tokget_linux_amd64.tar.gz'
wget -P /tmp 'https://github.com/i-core/tokget/releases/download/v1.2.0/tokget_linux_amd64.tar.gz'
tar -xzf /tmp/tokget_linux_amd64.tar.gz -C /usr/local/bin
```

Expand All @@ -61,7 +62,7 @@ Run `tokget -h` to see a list of available commands.
In terminal:

```bash
tokget login -e https://openid-connect-provider -c <client's ID> -r <client's redirect URL> -s openid,profile,email -u username --pwd-std
tokget login -e https://openid-connect-provider -c <client's ID> -r <client's redirect URL> -s openid,profile,email -u username -p password
```

**Note** Google Chrome must be in `$PATH`.
Expand All @@ -70,7 +71,7 @@ Via Docker:


```bash
docker run --name tokget --rm -it icoreru/tokget:v1.1.0 login -e https://openid-connect-provider -c <client ID> -r <client's redirect URL> -s openid,profile,email -u username -pwd-stdin
docker run --name tokget --rm -it icoreru/tokget:v1.2.0 login -e https://openid-connect-provider -c <client ID> -r <client's redirect URL> -s openid,profile,email -u username -p password
```

**Note** Image `icoreru/tokget` already contains Google Chrome so you don't need to run Google Chrome manually.
Expand Down Expand Up @@ -108,13 +109,90 @@ Via Docker:


```bash
docker run --name tokget --rm -it icoreru/tokget:v1.1.0 logout -e https://openid-connect-provider -t id_token
docker run --name tokget --rm -it icoreru/tokget:v1.2.0 logout -e https://openid-connect-provider -t id_token
```

### Serve

The command `serve` starts a web server. The web server has endpoints for logging in and out:

In the terminal:

```bash
tokget serve
```

Via Docker:


```bash
docker run --name tokget --rm -it -p 8080:8080 icoreru/tokget:v1.2.0 serve
```

After the web server started you can get a user's access token and ID token by sending a request to endpoint `/login`:

```bash
curl -X POST -H "Content-Type: application/json" -d '{"endpoint":"https://openid-connect-provider","clientId":"client ID","redirectUri":"redirect uri","scopes":"openid profile email","username":"user name","password":"user password"}' http://localhost:8080/login
```

A request's body must conforms the next JSON schema:

```yaml
type: object
properties:
endpoint:
type: string
clientId:
type: string
redirectUri:
type: string
scopes:
type: string
username:
type: string
password:
type: string
usernameField:
type: string
passwordField:
type: string
submitButton:
type: string
errorMessage:
type: string
required:
- endpoint
- clientId
- username
- password
}
```

And you can log a user out by sending a request to endpoint `/logout`:

```bash
curl -X POST -H "Content-Type: application/json" -d '{"endpoint": "https://openid-connect-provider","idToken": "ID token"}' http://localhost:8080/logout
```

A request's body must conforms the next JSON schema:

```yaml
type: object
properties:
endpoint:
type: string
idToken:
type: string
required:
- endpoint
- idToken
}
```

### Remote Google Chrome

By default `tokget` starts a new Google Chrome process. But you can use an existed Google Chrome process.
This Google Chrome process should be run with enabled debugger, for example:
This Google Chrome process should be run with an enabled debugger, for example:

```bash
chrome --no-sandbox --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222
Expand Down
114 changes: 98 additions & 16 deletions cmd/tokget/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,41 @@ import (
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"strings"

"github.com/i-core/rlog"
"github.com/i-core/routegroup"
"github.com/i-core/tokget/internal/errors"
"github.com/i-core/tokget/internal/log"
"github.com/i-core/tokget/internal/oidc"
"github.com/i-core/tokget/internal/stat"
"github.com/i-core/tokget/internal/web"
"github.com/kelseyhightower/envconfig"
"go.uber.org/zap"
)

// version will be filled at compile time.
var version = ""

// serveConfig is a server's configuration.
type serveConfig struct {
Listen string `default:":8080" desc:"a host and port to listen on (<host>:<port>)"`
Verbose bool `default:"false" desc:"a development mode"`
}

// The default values of the login operation's parameters.
const (
defaultRedirectURI = "http://localhost:3000"
defaultScopes = "openid,profile,email"
defaultUsernameField = "input[name=username]"
defaultPasswordField = "input[name=password]"
defaultSubmitButton = "button[type=submit]"
defaultErrorMessage = "p.message"
)

func main() {
var (
verboseLogin bool
Expand All @@ -33,15 +57,14 @@ func main() {
loginCmd := flag.NewFlagSet("login", flag.ExitOnError)
loginCmd.StringVar(&loginCnf.Endpoint, "e", "", "an OpenID Connect endpoint")
loginCmd.StringVar(&loginCnf.ClientID, "c", "", "an OpenID Connect client ID")
loginCmd.StringVar(&loginCnf.RedirectURI, "r", "http://localhost:3000", "an OpenID Connect client's redirect uri")
loginCmd.StringVar(&scopes, "s", "openid,profile,email", "OpenID Connect scopes")
loginCmd.StringVar(&loginCnf.RedirectURI, "r", defaultRedirectURI, "an OpenID Connect client's redirect uri")
loginCmd.StringVar(&scopes, "s", defaultScopes, "OpenID Connect scopes")
loginCmd.StringVar(&loginCnf.Username, "u", "", "a user's name")
loginCmd.StringVar(&loginCnf.Password, "p", "", "a user's password")
loginCmd.BoolVar(&loginCnf.PasswordStdin, "pwd-stdin", false, "a user's password from stdin")
loginCmd.StringVar(&loginCnf.UsernameField, "username-field", "input[name=username]", "a CSS selector of the username field on the login form")
loginCmd.StringVar(&loginCnf.PasswordField, "password-field", "input[name=password]", "a CSS selector of the password field on the login form")
loginCmd.StringVar(&loginCnf.SubmitButton, "submit-button", "button[type=submit]", "a CSS selector of the submit button on the login form")
loginCmd.StringVar(&loginCnf.ErrorMessage, "error-message", "p.message", "a CSS selector of an error message on the login form")
loginCmd.StringVar(&loginCnf.UsernameField, "username-field", defaultUsernameField, "a CSS selector of the username field on the login form")
loginCmd.StringVar(&loginCnf.PasswordField, "password-field", defaultPasswordField, "a CSS selector of the password field on the login form")
loginCmd.StringVar(&loginCnf.SubmitButton, "submit-button", defaultSubmitButton, "a CSS selector of the submit button on the login form")
loginCmd.StringVar(&loginCnf.ErrorMessage, "error-message", defaultErrorMessage, "a CSS selector of an error message on the login form")
loginCmd.BoolVar(&verboseLogin, "v", false, "verbose mode")

logoutCnf := &oidc.LogoutConfig{}
Expand All @@ -50,6 +73,11 @@ func main() {
logoutCmd.StringVar(&logoutCnf.IDToken, "t", "", "an ID token")
logoutCmd.BoolVar(&verboseLogout, "v", false, "verbose mode")

serveCnf := &serveConfig{}
serveCmd := flag.NewFlagSet("serve", flag.ExitOnError)
serveCmd.StringVar(&serveCnf.Listen, "l", "", "a host and port to listen on (<host>:<port>)")
serveCmd.BoolVar(&serveCnf.Verbose, "v", false, "verbose mode")

flag.Usage = func() {
fmt.Fprintln(os.Stderr, usage)
}
Expand All @@ -76,15 +104,54 @@ func main() {
}
chromeURL = args[1]
args = args[2:]
case serveCmd.Name():
if err := envconfig.Process("tokget", serveCnf); err != nil {
fmt.Fprintf(os.Stderr, "Invalid configuration: %s\n", err)
os.Exit(1)
}

serveCmd.Parse(args[1:])

logFunc := zap.NewProduction
if serveCnf.Verbose {
logFunc = zap.NewDevelopment
}
log, err := logFunc()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create logger: %s\n", err)
os.Exit(1)
}

router := routegroup.NewRouter(rlog.NewMiddleware(log))
loginFn := func(ctx context.Context, cnf *oidc.LoginConfig) (*oidc.LoginData, error) {
return oidc.Login(ctx, chromeURL, cnf)
}
logoutFn := func(ctx context.Context, cnf *oidc.LogoutConfig) error {
return oidc.Logout(ctx, chromeURL, cnf)
}
defaults := web.Defaults{
RedirectURI: defaultRedirectURI,
Scopes: strings.ReplaceAll(defaultScopes, ",", " "),
UsernameField: defaultUsernameField,
PasswordField: defaultPasswordField,
SubmitButton: defaultSubmitButton,
ErrorMessage: defaultErrorMessage,
}
router.AddRoutes(web.NewHandler(defaults, loginFn, logoutFn, rlog.FromContext), "")
router.AddRoutes(stat.NewHandler(version), "/stat")

log = log.Named("main")
log.Info("Tokget started", zap.Any("config", serveCnf), zap.String("version", version))
log.Fatal("Tokget finished", zap.Error(http.ListenAndServe(serveCnf.Listen, router)))
os.Exit(0)
case loginCmd.Name():
loginCmd.Parse(args[1:])

loginCnf.Scopes = strings.ReplaceAll(scopes, ",", " ")

ctx := context.Background()
if verboseLogin {
ctx = log.WithDebugger(ctx, log.VerboseDebugger)
}
ctx := withInterrupt(context.Background())
ctx = log.WithLogger(ctx, verboseLogin)

v, err := oidc.Login(ctx, chromeURL, loginCnf)
if err != nil {
if errors.Cause(err) != context.Canceled {
Expand All @@ -102,10 +169,9 @@ func main() {
case logoutCmd.Name():
logoutCmd.Parse(args[1:])

ctx := context.Background()
if verboseLogout {
ctx = log.WithDebugger(ctx, log.VerboseDebugger)
}
ctx := withInterrupt(context.Background())
ctx = log.WithLogger(ctx, verboseLogout)

err := oidc.Logout(ctx, chromeURL, logoutCnf)
if err != nil {
if errors.Cause(err) != context.Canceled {
Expand All @@ -124,15 +190,31 @@ func main() {
os.Exit(1)
}

// withInterrupt returns a new context that will be canceled when the program receives an interruption signal (SIGINT).
func withInterrupt(parent context.Context) context.Context {
ctx, cancel := context.WithCancel(parent)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
select {
case <-c:
cancel()
case <-ctx.Done():
}
}()
return ctx
}

const usage = `
usage: tokget [options] <command> [options]

Options:
--remote-chrome <url> A remote Google Chrome's url
--remote-chrome <url> A remote Google Chrome's URL

Commands:
login Logs a user in and returns its access token and ID token.
logout Logs a user out.
serve Starts a web server that allows logging in and out.
version Prints version of the tool.
help Prints help about the tool.
`
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ go 1.12
require (
github.com/chromedp/cdproto v0.0.0-20190429085128-1aa4f57ff2a9
github.com/chromedp/chromedp v0.3.0
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
github.com/i-core/rlog v1.0.0
github.com/i-core/routegroup v1.0.0
github.com/justinas/nosurf v0.0.0-20190416172904-05988550ea18
github.com/kelseyhightower/envconfig v1.4.0
go.uber.org/zap v1.10.0
)
Loading