-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e20a651
Showing
11 changed files
with
766 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# Ignore hidden files except this .gitignore | ||
.* | ||
!.gitignore | ||
|
||
# Ignore built binaries | ||
utok | ||
bin/ | ||
|
||
# Ignore Postman/Hoppscotch collections: they contain | ||
# sensitive data (i.e. tokens)! | ||
*.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# uTok: A microscopic OpenID connect client | ||
uTok aims to be a micro ($\mu$) client for generating access tokens through OpenID Connect's | ||
Device Authorization Flow. | ||
|
||
As hinted by its default settings, `uTok`'s main target is the | ||
[WLCG's main Indigo IAM instance](https://wlcg.cloud.cnaf.infn.it). You can find a bit | ||
of documentation on its APIs and such [here](https://indigo-iam.github.io/v/current/). | ||
Even though we haven't tested it, `utok` might work with other *issuers*: we didn't really | ||
do anything 'special' for targetting Indigo IAM when it comes to token generation. | ||
|
||
Bear in mind the official client for Indigo IAM is [`oidc-agent`](https://github.com/indigo-dc/oidc-agent), | ||
but we found it a bit 'aggressive' in its pursue of `ssh-agent`'s behavior and, after a lot of digging, | ||
we didn't manage to get it to work on newer macOS versions or on CentOS 7... | ||
|
||
This client's interface is rather self explanatory: running `utok` with no arguments will show some | ||
pointers to make use of `utok`. | ||
|
||
## Installation | ||
You can just download the latest build for your platform and place the binary anywhere on your `PATH`. | ||
|
||
Uninstalling `utok` is a matter of removing that binary! | ||
|
||
## Getting the first token | ||
In order to get a token you first need to create a client: | ||
|
||
$ utok cli create | ||
|
||
This will create `~/.utok/client.json` containing the reply's contents. This reply will | ||
also be shown on screen. | ||
|
||
After that, you can generate tokens with: | ||
|
||
$ utok token | ||
|
||
This instructs `utok` to read the contents of `~/.utok/client.json` to then try to generate a | ||
token. If none have been generated previously, the Device Authorization Flow will be triggered | ||
so that you'll need to navigate to a particular URL and input a code: all these instructions | ||
will be shown on screen. The generated token will be stored on `~/.utok/token.json`. | ||
|
||
After generating the first token, `utok` will leverage the **refresh token** embedded in the | ||
initial one to re-generate access tokens at will. However, this is completely transparent: | ||
the user need only run `utok token`. Fresh tokens will be stored on `~/.utok/token_fresh.json`. | ||
|
||
After a client is no longer needed, it can be deleted with: | ||
|
||
$ utok cli delete | ||
|
||
And... that's it really! Happy tokening! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
"strings" | ||
"syscall" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/zitadel/oidc/v3/pkg/client" | ||
httphelper "github.com/zitadel/oidc/v3/pkg/http" | ||
) | ||
|
||
const CLIENT_CONF string = "client.json" | ||
|
||
var cliCmd = &cobra.Command{ | ||
Use: "cli", | ||
Short: "Create or delete the OIDC client.", | ||
} | ||
|
||
var cliCreateCmd = &cobra.Command{ | ||
Use: "create", | ||
Short: "Create an OIDC client.", | ||
Run: func(cmd *cobra.Command, args []string) { | ||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT) | ||
defer stop() | ||
createdClient, err := createClient(ctx) | ||
if err != nil { | ||
fmt.Printf("error creating the client: %v\n", err) | ||
os.Exit(-1) | ||
} | ||
|
||
prettyClient, err := json.MarshalIndent(createdClient, "", " ") | ||
if err != nil { | ||
fmt.Printf("error encoding client: %v\n", err) | ||
os.Exit(-1) | ||
} | ||
|
||
fmt.Printf("%s\n", prettyClient) | ||
if err := writeFile(CLIENT_CONF, append(prettyClient, "\n"...)); err != nil { | ||
fmt.Printf("error saving the client to %s: %v", CLIENT_CONF, err) | ||
} | ||
|
||
os.Exit(0) | ||
}, | ||
} | ||
|
||
var cliDeleteCmd = &cobra.Command{ | ||
Use: "delete", | ||
Short: "Delete the OIDC client.", | ||
Run: func(cmd *cobra.Command, args []string) { | ||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT) | ||
defer stop() | ||
if err := deleteClient(ctx); err != nil { | ||
fmt.Printf("error deleting the client: %v\n", err) | ||
os.Exit(-1) | ||
} | ||
fmt.Printf("client deleted successfully!\n") | ||
|
||
if err := removeFile(CLIENT_CONF); err != nil { | ||
fmt.Printf("error removing the client configuration: %v\n", err) | ||
} | ||
|
||
if err := removeFile(TOKEN_CONF); err != nil { | ||
fmt.Printf("error removing the original token: %v\n", err) | ||
} | ||
|
||
if err := removeFile(REFRESH_TOKEN_CONF); err != nil { | ||
fmt.Printf("error removing the refreshed token: %v\n", err) | ||
} | ||
|
||
os.Exit(0) | ||
}, | ||
} | ||
|
||
func createClient(ctx context.Context) (Client, error) { | ||
var createdClient Client | ||
|
||
oidcConfig, err := client.Discover(ctx, issuer, httphelper.DefaultHTTPClient) | ||
if err != nil { | ||
return createdClient, fmt.Errorf("error discovering endpoints: %v", err) | ||
} | ||
|
||
cliConf := baseClientConf | ||
|
||
hostname, err := os.Hostname() | ||
if err == nil { | ||
clientName = clientName + ":" + hostname | ||
} | ||
cliConf.ClientName = clientName | ||
cliConf.Contacts = strings.Split(clientContacts, ",") | ||
cliConf.Scope = strings.Join(strings.Split(scopes, ","), " ") | ||
|
||
encPayload, err := json.Marshal(cliConf) | ||
if err != nil { | ||
return createdClient, err | ||
} | ||
|
||
req, err := http.NewRequest("POST", oidcConfig.RegistrationEndpoint, bytes.NewBuffer(encPayload)) | ||
if err != nil { | ||
fmt.Println(err) | ||
return createdClient, fmt.Errorf("error creating request: %v", err) | ||
} | ||
req.Header.Add("Content-Type", "application/json") | ||
|
||
res, err := httphelper.DefaultHTTPClient.Do(req) | ||
if err != nil { | ||
fmt.Println(err) | ||
return createdClient, fmt.Errorf("error making the request: %v", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
fmt.Println(err) | ||
return createdClient, fmt.Errorf("error reading back the reply: %v", err) | ||
} | ||
|
||
if err := json.Unmarshal(body, &createdClient); err != nil { | ||
return createdClient, fmt.Errorf("error decoding reply: %v [%s]", err, body) | ||
} | ||
|
||
return createdClient, nil | ||
} | ||
|
||
func deleteClient(ctx context.Context) error { | ||
cliConfRaw, err := readFile(CLIENT_CONF) | ||
if err != nil { | ||
return fmt.Errorf("error reading client conf: %v", err) | ||
} | ||
|
||
var cli Client | ||
if err := json.Unmarshal(cliConfRaw, &cli); err != nil { | ||
return fmt.Errorf("error parsing client conf: %v", err) | ||
} | ||
|
||
oidcConfig, err := client.Discover(ctx, issuer, httphelper.DefaultHTTPClient) | ||
if err != nil { | ||
return fmt.Errorf("error discovering endpoints: %v", err) | ||
} | ||
|
||
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/%s", oidcConfig.RegistrationEndpoint, cli.ClientId), nil) | ||
if err != nil { | ||
fmt.Println(err) | ||
return fmt.Errorf("error creating request: %v", err) | ||
} | ||
req.Header.Add("Authorization", "Bearer "+cli.RegistrationAccessToken) | ||
|
||
res, err := httphelper.DefaultHTTPClient.Do(req) | ||
if err != nil { | ||
fmt.Println(err) | ||
return fmt.Errorf("error making the request: %v", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
if res.StatusCode != http.StatusNoContent { | ||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return fmt.Errorf("couldn't open the reply's body: %v", err) | ||
} | ||
return fmt.Errorf("%s [%d]", body, res.StatusCode) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/zitadel/oidc/v3/pkg/client" | ||
httphelper "github.com/zitadel/oidc/v3/pkg/http" | ||
"github.com/zitadel/oidc/v3/pkg/oidc" | ||
) | ||
|
||
var configCmd = &cobra.Command{ | ||
Use: "config", | ||
Short: "Show the issuer's OIDC config.", | ||
Run: func(cmd *cobra.Command, args []string) { | ||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT) | ||
defer stop() | ||
|
||
oidcConf, err := getOIDCConfig(ctx) | ||
if err != nil { | ||
fmt.Printf("error getting the configuration: %v\n", err) | ||
os.Exit(-1) | ||
} | ||
|
||
prettyConf, err := json.MarshalIndent(oidcConf, "", " ") | ||
if err != nil { | ||
fmt.Printf("error encoding the configuration: %v\n", err) | ||
os.Exit(-1) | ||
} | ||
|
||
fmt.Printf("%s\n", prettyConf) | ||
|
||
os.Exit(0) | ||
}, | ||
} | ||
|
||
func getOIDCConfig(ctx context.Context) (oidc.DiscoveryConfiguration, error) { | ||
oidcConfig, err := client.Discover(ctx, issuer, httphelper.DefaultHTTPClient) | ||
if err != nil { | ||
return oidc.DiscoveryConfiguration{}, fmt.Errorf("error discovering endpoints: %v", err) | ||
} | ||
|
||
return *oidcConfig, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package main | ||
|
||
import "log" | ||
|
||
var ( | ||
issuer string | ||
clientName string | ||
clientContacts string | ||
scopes string | ||
forceRecreate bool | ||
) | ||
|
||
func init() { | ||
// Disable Cobra's completions. | ||
rootCmd.CompletionOptions.DisableDefaultCmd = true | ||
|
||
// Configure the main logger | ||
log.SetFlags(log.Lshortfile) | ||
|
||
// Define global flags | ||
rootCmd.PersistentFlags().StringVar(&issuer, "iss", "https://wlcg.cloud.cnaf.infn.it/", "The issuer URL.") | ||
rootCmd.PersistentFlags().StringVar(&clientName, "cli-name", "uTok-cli", "OIDC client name.") | ||
rootCmd.PersistentFlags().StringVar(&clientContacts, "cli-contacts", "foo@faa.com", "Comma-separated OIDC client contacts.") | ||
rootCmd.PersistentFlags().StringVar(&scopes, "scopes", | ||
"storage.read:/atlasdatadisk/SAM/,openid,offline_access", "Comma separated scopes to request.") | ||
|
||
tokenCmd.PersistentFlags().BoolVar(&forceRecreate, "recreate", false, "Force token recreation even if one is present.") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
module github.com/pcolladosoto/utok | ||
|
||
go 1.21 | ||
|
||
require ( | ||
github.com/spf13/cobra v1.8.0 | ||
github.com/zitadel/oidc v1.13.5 | ||
github.com/zitadel/oidc/v3 v3.10.3 | ||
) | ||
|
||
require ( | ||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect | ||
github.com/golang/protobuf v1.5.3 // indirect | ||
github.com/google/uuid v1.5.0 // indirect | ||
github.com/gorilla/schema v1.2.0 // indirect | ||
github.com/gorilla/securecookie v1.1.2 // indirect | ||
github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
github.com/muhlemmer/gu v0.3.1 // indirect | ||
github.com/sirupsen/logrus v1.9.3 // indirect | ||
github.com/spf13/pflag v1.0.5 // indirect | ||
github.com/zitadel/logging v0.5.0 // indirect | ||
github.com/zitadel/schema v1.3.0 // indirect | ||
golang.org/x/crypto v0.18.0 // indirect | ||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect | ||
golang.org/x/oauth2 v0.16.0 // indirect | ||
golang.org/x/sys v0.16.0 // indirect | ||
golang.org/x/text v0.14.0 // indirect | ||
google.golang.org/appengine v1.6.8 // indirect | ||
google.golang.org/protobuf v1.31.0 // indirect | ||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect | ||
) |
Oops, something went wrong.