Commit 8fd469d1 authored by Simon Schürg's avatar Simon Schürg 🚀
Browse files

Major cleanup, refactoring and simplification

No more local datastore for now
parent 56feb7ae
......@@ -2,11 +2,8 @@ package main
import (
"git.schuerg.net/simon/actl/cmd"
"git.schuerg.net/simon/actl/internal"
)
func main() {
internal.InitDb()
defer internal.CloseDb()
cmd.Execute()
}
package cmd
import (
"git.schuerg.net/simon/actl/internal"
"github.com/spf13/cobra"
)
var clearCmd = &cobra.Command{
Use: "clear",
Short: "Clears the local data store.",
Long: `Clears the local data store. Deletes the bolt db file.`,
Run: func(cmd *cobra.Command, args []string) {
internal.ClearDb()
},
}
func init() {
rootCmd.AddCommand(clearCmd)
}
......@@ -12,7 +12,6 @@ var decodeCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
token := args[0]
internal.UpsertJWT(token)
internal.PrettyPrintDecodedJWT(token)
},
}
......
......@@ -14,7 +14,7 @@ var discoverCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
issuer := args[0]
oidcMetadata := internal.GetOidcMetadata(issuer, true)
oidcMetadata := internal.FetchOidcMetadata(issuer)
oidcMetaJSON, err := json.Marshal(oidcMetadata)
internal.FatalOnError(err)
internal.PrettyPrintJSON(oidcMetaJSON)
......
......@@ -2,11 +2,7 @@ package cmd
import (
"fmt"
"os"
"git.schuerg.net/simon/actl/internal"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
......@@ -21,65 +17,9 @@ var statusCmd = &cobra.Command{
} else {
fmt.Printf("%26s\n", "No actl config file found. Using only defaults, env vars and cli flags.")
}
fmt.Printf("%26s %s\n\n", "Cached data is located at:", internal.GetDbPath())
fmt.Println("JWTs:")
printTokenTable()
fmt.Println("JWKs:")
printJwkTable()
fmt.Println("OpenID Connect Providers:")
printIssuerTable()
},
}
func init() {
rootCmd.AddCommand(statusCmd)
}
func printIssuerTable() {
issuers := internal.GetAllIssuer()
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"issuer", "jwks_uri"})
rows := []table.Row{}
for _, issuer := range issuers {
rows = append(rows, table.Row{issuer.Issuer, issuer.JwksURI})
}
t.AppendRows(rows)
t.SetStyle(table.StyleLight)
t.Style().Format.Header = text.FormatLower
t.Render()
}
func printJwkTable() {
jwks := internal.GetAllJWK()
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"kid", "kty", "alg", "issuer", "key_ops"})
rows := []table.Row{}
for _, jwk := range jwks {
rows = append(rows, table.Row{jwk.Kid, jwk.Kty, jwk.Alg, jwk.Issuer, jwk.KeyOps})
}
t.AppendRows(rows)
t.SetStyle(table.StyleLight)
t.Style().Format.Header = text.FormatLower
t.Render()
}
func printTokenTable() {
jwts := internal.GetAllJWT()
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"kid", "typ", "alg", "sub (preferred_username)", "issuer", "exp"})
rows := []table.Row{}
for _, jwt := range jwts {
joseHeader := jwt.GetJOSEHeader()
joseRegClaims := jwt.GetRegisteredClaims()
oidcStdClaims := jwt.GetOidcStandardClaims()
rows = append(rows, table.Row{joseHeader.KeyID, joseHeader.Typ, joseHeader.Alg, fmt.Sprintf("%s (%s)", joseRegClaims.Subject, oidcStdClaims.PreferredUsername), joseRegClaims.Issuer, joseRegClaims.ExpirationTime})
}
t.AppendRows(rows)
t.SetStyle(table.StyleLight)
t.Style().Format.Header = text.FormatLower
t.Render()
}
......@@ -12,7 +12,6 @@ var userinfoCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
rawToken := args[0]
internal.UpsertJWT(rawToken)
userinfoToken := internal.UserInfo(rawToken)
internal.PrettyPrintDecodedJWT(userinfoToken.Encoded)
},
......
......@@ -4,18 +4,15 @@ go 1.15
require (
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2
github.com/asdine/storm/v3 v3.2.1
github.com/atotto/clipboard v0.1.4
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/fatih/color v1.12.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-resty/resty/v2 v2.6.0
github.com/golang/protobuf v1.5.2 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e // indirect
github.com/int128/oauth2cli v1.13.0
github.com/jedib0t/go-pretty/v6 v6.2.2
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pascaldekloe/jwt v1.10.0
......@@ -29,12 +26,11 @@ require (
github.com/spf13/cobra v1.1.3
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.7.1
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210531080801-fdfd190a6549 // indirect
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
......
This diff is collapsed.
......@@ -22,7 +22,7 @@ import (
// RefreshToken uses an existing refresh token to retrieve a new TokenSet
// See https://tools.ietf.org/html/rfc6749#section-6
func RefreshToken(issuer, clientID, refreshToken string) *TokenSet {
provider := GetOidcMetadata(issuer, true)
provider := FetchOidcMetadata(issuer)
payload := url.Values{}
payload.Add("grant_type", "refresh_token")
payload.Add("client_id", clientID)
......@@ -43,7 +43,7 @@ func RefreshToken(issuer, clientID, refreshToken string) *TokenSet {
// ClientCredenitalsAuth uses a client id and client secret to retrieve a TokenSet
// See https://tools.ietf.org/html/rfc6749#section-4.4
func ClientCredenitalsAuth(issuer, clientID, clientSecret string) *TokenSet {
provider := GetOidcMetadata(issuer, true)
provider := FetchOidcMetadata(issuer)
payload := url.Values{}
payload.Add("grant_type", "client_credentials")
payload.Add("client_id", clientID)
......@@ -64,7 +64,7 @@ func ClientCredenitalsAuth(issuer, clientID, clientSecret string) *TokenSet {
// ResourceOwnerCredentialsAuth uses a username and password to retrieve a TokenSet
// See https://tools.ietf.org/html/rfc6749#section-10.7
func ResourceOwnerCredentialsAuth(issuer, clientID, username, password string) *TokenSet {
provider := GetOidcMetadata(issuer, true)
provider := FetchOidcMetadata(issuer)
payload := url.Values{}
payload.Add("grant_type", "password")
payload.Add("client_id", clientID)
......@@ -90,7 +90,7 @@ func AuthorizationCodeAuth(clientID, clientSecret, openidIssuerURL string) *Toke
FatalOnError(errors.Wrap(err, "Failed creating a new PKCE"))
ready := make(chan string, 1)
defer close(ready)
provider := GetOidcMetadata(openidIssuerURL, true)
provider := FetchOidcMetadata(openidIssuerURL)
cfg := oauth2cli.Config{
OAuth2Config: oauth2.Config{
ClientID: clientID,
......@@ -184,7 +184,7 @@ func TokenRevocation() {
// The result is a userinfo token -- also a JWT.
func UserInfo(accessToken string) *Token {
token := DecodeToken([]byte(accessToken))
oidcMeta := GetOidcMetadata(token.GetRegisteredClaims().Issuer, true)
oidcMeta := FetchOidcMetadata(token.GetRegisteredClaims().Issuer)
req, err := http.NewRequest("GET", oidcMeta.UserinfoEndpoint, nil)
FatalOnError(err)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
......
package internal
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/asdine/storm/v3"
"github.com/mitchellh/go-homedir"
"github.com/pascaldekloe/jwt"
)
// Metadata describes the database metadata
type Metadata struct {
ID string
SchemaVersion string
CreatedAt time.Time
}
// SchemaVersion defines the database schema used by this version of actl.
// It will be increased on database schema changes in order to react conflicts.
const SchemaVersion = "0.0.1"
// db references the local database
var db *storm.DB
// GetDbPath returns the file path of the boltdb file
// which is or should be used by actl.
func GetDbPath() string {
dbName := "actl.boltdb"
xdgCacheHome := os.Getenv("XDG_CACHE_HOME")
if xdgCacheHome == "" {
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// ${HOME}/.cache should be the default XDG_CACHE_HOME
xdgCacheHome = fmt.Sprintf("%s/.cache", home)
}
dbPath := fmt.Sprintf("%s/actl", xdgCacheHome)
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
os.Mkdir(dbPath, 0700)
}
return filepath.FromSlash(fmt.Sprintf("%s/%s", dbPath, dbName))
}
// InitDb ensures the existence of the local database and the schema.
func InitDb() {
var err error
db, err = storm.Open(GetDbPath())
FatalOnError(err)
if !doesSchemaVersionMatch() {
log.Fatalln("Database schma version mismatch")
}
}
func doesSchemaVersionMatch() bool {
var dbMetadata Metadata
err := db.One("ID", "schema", &dbMetadata)
if err != nil {
if errors.Is(err, storm.ErrNotFound) {
err = db.Save(&Metadata{
ID: "schema",
SchemaVersion: SchemaVersion,
CreatedAt: time.Now(),
})
FatalOnError(err)
return true
}
FatalOnError(err)
}
return dbMetadata.SchemaVersion == SchemaVersion
}
// CloseDb closes the local database connection gracefully
func CloseDb() {
err := db.Close()
FatalOnError(err)
}
// UpsertJWT creates a Token object from a given JWT string
// and persists it in the local database.
func UpsertJWT(encodedToken string) *Token {
claims, err := jwt.ParseWithoutCheck([]byte(encodedToken))
FatalOnError(err)
token := Token{
Encoded: encodedToken,
}
err = json.Unmarshal(claims.RawHeader, &token.Header)
FatalOnError(err)
err = json.Unmarshal(claims.Raw, &token.Payload)
FatalOnError(err)
err = db.Save(&token)
FatalOnError(err)
return &token
}
// ClearDb closes and deletes the whole database.
// Be careful with this method, it is dangerous!
func ClearDb() {
CloseDb()
err := os.Remove(GetDbPath())
FatalOnError(err)
}
// UpsertJWK persists a jwk object to the database
func UpsertJWK(jwk *JWK) *JWK {
err := db.Save(jwk)
FatalOnError(err)
return jwk
}
// UpsertIssuer persists the OpenID Provider metadata fetched from an issuer URL
func UpsertIssuer(oidcMetadata *OpenIDProviderMetadata) *OpenIDProviderMetadata {
err := db.Save(oidcMetadata)
FatalOnError(err)
return oidcMetadata
}
// GetAllJWT fetches all known JWTs from the local database
func GetAllJWT() []Token {
var allToken []Token
err := db.AllByIndex("Encoded", &allToken)
FatalOnError(err)
return allToken
}
// GetAllJWK fetches all known JWKs from the local database
func GetAllJWK() []JWK {
var allJWK []JWK
err := db.AllByIndex("Kid", &allJWK)
FatalOnError(err)
return allJWK
}
// GetAllIssuer fetches all known OpenID Connect issuers from the local database
func GetAllIssuer() []OpenIDProviderMetadata {
var allIssuer []OpenIDProviderMetadata
err := db.AllByIndex("Issuer", &allIssuer)
FatalOnError(err)
return allIssuer
}
package internal
import "log"
import (
"fmt"
"log"
"os"
"github.com/go-resty/resty/v2"
)
// FatalOnError checks the err parameter and terminates
// the process if an error exists
......@@ -9,3 +15,22 @@ func FatalOnError(err error) {
log.Fatal(err)
}
}
// LogRestyResp logs a resty http response in only two lines
func LogRestyResp(resp *resty.Response, err error) {
exitOnErr := true
verbose := true
if err != nil {
fmt.Println(err)
if exitOnErr {
os.Exit(1)
}
}
if verbose || resp.IsError() {
fmt.Println("HTTP", resp.Request.Method, resp.Request.URL)
fmt.Println(resp.Status()+":", resp)
if exitOnErr && resp.IsError() {
os.Exit(1)
}
}
}
package internal
import (
"encoding/base64"
"encoding/binary"
)
func base64ToInt(s string) (uint32, error) {
a, e := base64.StdEncoding.DecodeString(s)
if e != nil {
return 0, e
}
return binary.LittleEndian.Uint32(append(a, 0)), nil
}
package internal
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io/ioutil"
"log"
"math/big"
"net/http"
"net/url"
"strings"
"time"
"github.com/asdine/storm/v3"
"github.com/go-resty/resty/v2"
"github.com/pascaldekloe/jwt"
"github.com/pkg/errors"
)
......@@ -79,20 +84,21 @@ type OpenIDProviderMetadata struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
type JWKS struct {
Keys []JWK `json:"keys"`
}
// JWK - JSON Web Key
// A JWK is a JSON object that represents a cryptographic key. The members of
// the object represent properties of the key, including its value.
// See https://tools.ietf.org/html/rfc7517#section-4
type JWK struct {
// Key ID
Kid string `json:"kid" storm:"id"`
Kid string `json:"kid"`
// Key Type
Kty string `json:"kty"`
// Public Key Use (sig or enc)
Use string `json:"use"`
// Key Operations
// sign, verify, encrypt, decrypt, wrapKey, unwrapKey, deriveKey, deriveBits
KeyOps string `json:"key_ops"`
......@@ -101,21 +107,21 @@ type JWK struct {
// See https://tools.ietf.org/html/rfc7518
Alg string `json:"alg"`
// X.509 URL
X5u string `json:"x5u"`
// Public Key Use (sig or enc)
Use string `json:"use"`
// X.509 Certificate Chain
X5c []string `json:"x5c"`
N string `json:"n"`
// X.509 Certificate SHA-1 Thumbprint
X5t string `json:"x5t"`
E string `json:"e"`
// X.509 Certificate SHA-256 Thumbprint
X5tS256 string `json:"x5t#S256"`
// X.509 URL
X5C []string `json:"x5c"`
Issuer string
// X.509 Certificate SHA-1 Thumbprint
X5T string `json:"x5t"`
CreatedAt time.Time
// X.509 Certificate SHA-256 Thumbprint
X5TS256 string `json:"x5t#S256"`
}
// JOSEHeader - the JSON Object Signing and Encryption Header is
......@@ -246,25 +252,20 @@ type JWKSet struct {
// DiscoverOidcMetadata fetches OpenID Connect Provider configuration
// from an issuer URL
func fetchOidcMetadata(issuerURL string) OpenIDProviderMetadata {
func FetchOidcMetadata(issuerURL string) OpenIDProviderMetadata {
openidConfigURL := strings.TrimRight(issuerURL, "/") + "/.well-known/openid-configuration"
url, err := url.ParseRequestURI(openidConfigURL)
FatalOnError(err)
resp, err := http.Get(url.String())
FatalOnError(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
FatalOnError(err)
client := resty.New()
resp, err := client.R().Get(openidConfigURL)
LogRestyResp(resp, err)
var openIDProviderMetadata OpenIDProviderMetadata
err = json.Unmarshal(body, &openIDProviderMetadata)
err = json.Unmarshal(resp.Body(), &openIDProviderMetadata)
FatalOnError(err)
// fmt.Println(openIDProviderMetadata)
return openIDProviderMetadata
}
// FetchJWKSet fetches all JWKs from a given OpenID Connect Cert URL
func fetchJWKSet(issuer string) *JWKSet {
oidcMetadata := GetOidcMetadata(issuer, true)
oidcMetadata := FetchOidcMetadata(issuer)
url, err := url.ParseRequestURI(oidcMetadata.JwksURI)
FatalOnError(err)
resp, err := http.Get(url.String())
......@@ -276,46 +277,37 @@ func fetchJWKSet(issuer string) *JWKSet {
err = json.Unmarshal(body, &jwkSet)
FatalOnError(err)
// fmt.Println(jwkSet)
for _, jwk := range jwkSet.Keys {
jwk.CreatedAt = time.Now()
jwk.Issuer = issuer
UpsertJWK(&jwk)
}
// for _, jwk := range jwkSet.Keys {
// }
return &jwkSet
}
// GetOidcMetadata returns the OpenID Connect Provider Metadata of a given issuer
func GetOidcMetadata(issuer string, useCache bool) *OpenIDProviderMetadata {
var oidcMetadata OpenIDProviderMetadata
if useCache {
err := db.One("Issuer", issuer, &oidcMetadata)
if err == nil {
return &oidcMetadata
} else if err != storm.ErrNotFound {
FatalOnError(errors.Wrap(err, "Error reading from db"))
}
func JWKToPEM(jwk JWK) string {
if jwk.Kty != "RSA" {
log.Fatal("invalid key type:", jwk.Kty)
}
oidcMetadata = fetchOidcMetadata(issuer)
UpsertIssuer(&oidcMetadata)
return &oidcMetadata
}
// GetJWK searches for a JWK by a given kid (key id)
func GetJWK(kid, issuer string, useCache bool) *JWK {
var jwk JWK
if useCache {
err := db.One("kid", kid, jwk)
if err == nil {
return &jwk
} else if err != storm.ErrNotFound {
FatalOnError(err)
}
// decode the base64 bytes for n
nb, err := base64.RawURLEncoding.DecodeString(jwk.N)
if err != nil {
log.Fatal(err)
}
e, err := base64ToInt(jwk.E)
if err != nil {
log.Fatal(err)
}
pk := &rsa.PublicKey{
N: new(big.Int).SetBytes(nb),
E: int(e),
}
der, err := x509.MarshalPKIXPublicKey(pk)
if err != nil {
log.Fatal(err)
}
jwkSet := fetchJWKSet(issuer)
for _, currJwk := range jwkSet.Keys {
if currJwk.Kid == kid {
return &currJwk
}
block := &pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: der,
}
return nil
var out bytes.Buffer
pem.Encode(&out, block)
return out.String()
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment