diff --git a/cmd/jwks.go b/cmd/jwks.go new file mode 100644 index 0000000..e59372b --- /dev/null +++ b/cmd/jwks.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "os" + + "github.com/cloudentity/oauth2c/internal/jwks" + "github.com/go-jose/go-jose/v3" + "github.com/spf13/cobra" +) + +var jwksCmd = &cobra.Command{ + Use: "jwks", + Short: "JWKs operations", +} + +var jwksConfig jwks.Config + +var generateJwkCmd = &cobra.Command{ + Use: "generate", + Short: "Generate JWK", + Long: ` +# Supported algorithms + +Signing | Algorithm +------------------------- | ------------------------- +RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 +RSASSA-PSS | PS256, PS384, PS512 +ECDSA | ES256, ES384, ES512 + +Encryption | Algorithm +------------------------- | ------------------------- +RSA-PKCS#1v1.5 | RSA1_5 +RSA-OAEP | RSA-OAEP, RSA-OAEP-256 +ECDH-ES | ECDH-ES +ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW +`, + Run: func(cmd *cobra.Command, args []string) { + var ( + jwk jose.JSONWebKey + err error + ) + + if jwk, err = jwks.Generate(jwksConfig); err != nil { + LogError(err) + os.Exit(1) + } + + LogHeader("Generate JWK") + + LogSection("Private JWK") + LogJson(jwk) + + LogSection("Public JWK") + LogJson(jwk.Public()) + + LogSection("Private Key") + LogKey(jwk.Key) + + LogSection("Public Key") + LogKey(jwk.Public().Key) + }, +} + +func init() { + generateJwkCmd.Flags().StringVar(&jwksConfig.Type, "type", "rsa", "key type (rsa, ec)") + generateJwkCmd.Flags().StringVar(&jwksConfig.Alg, "alg", "", "key algorithm") + generateJwkCmd.Flags().IntVar(&jwksConfig.Size, "size", 2048, "key size (2048, 3072, 4096)") + generateJwkCmd.Flags().IntVar(&jwksConfig.Curve, "curve", 256, "key curve (224, 256, 384, 521)") + generateJwkCmd.Flags().StringVar(&jwksConfig.Use, "use", "sig", "key use (sig, enc)") + + jwksCmd.AddCommand(generateJwkCmd) +} diff --git a/cmd/log.go b/cmd/log.go index f3ff328..99be092 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -327,11 +327,13 @@ func LogRequestObject(r oauth2.Request) { pterm.Println() if r.SigningKey != nil { - LogKey("Signing key", r.SigningKey) + pterm.Println("Signing key") + LogKey(r.SigningKey) } if r.EncryptionKey != nil { - LogKey("Encryption key", r.EncryptionKey) + pterm.Println("Encryption key") + LogKey(r.EncryptionKey) } } } @@ -364,14 +366,13 @@ func LogAssertion(request oauth2.Request, title string, name string) { LogJson(claims) pterm.Println("") - LogKey("Signing key", request.SigningKey) + pterm.Println("Signing key") + LogKey(request.SigningKey) } -func LogKey(name string, key interface{}) { +func LogKey(key interface{}) { var err error - pterm.Println(name) - switch key := key.(type) { case *rsa.PublicKey: p := bytes.Buffer{} diff --git a/cmd/oauth2.go b/cmd/oauth2.go index 5730f06..023196d 100644 --- a/cmd/oauth2.go +++ b/cmd/oauth2.go @@ -23,6 +23,19 @@ var ( noPrompt bool ) +var example = `oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \ + --client-id cauktionbud6q8ftlqq0 \ + --client-secret HCwQ5uuUWBRHd04ivjX5Kl0Rz8zxMOekeLtqzki0GPc \ + --grant-type client_credentials \ + --auth-method client_secret_basic \ + --scopes introspect_tokens,revoke_tokens` + +var desc = `oauth2c is a command-line tool for interacting with OAuth 2.0 authorization servers. + +Its goal is to make it easy to fetch access tokens using any grant type or client authentication method. + +It is compliant with almost all basic and advanced OAuth 2.0, OIDC, OIDF FAPI and JWT profiles.` + type OAuth2Cmd struct { *cobra.Command } @@ -32,9 +45,11 @@ func NewOAuth2Cmd(version, commit, date string) (cmd *OAuth2Cmd) { cmd = &OAuth2Cmd{ Command: &cobra.Command{ - Use: "oauth2c [issuer url]", - Short: "User-friendly command-line for OAuth2", - Args: cobra.ExactArgs(1), + Use: "oauth2c issuerURL", + Short: "User-friendly command-line for OAuth2", + Example: example, + Long: desc, + Args: cobra.ExactArgs(1), }, } @@ -42,46 +57,47 @@ func NewOAuth2Cmd(version, commit, date string) (cmd *OAuth2Cmd) { cmd.AddCommand(NewVersionCmd(version, commit, date)) cmd.AddCommand(docsCmd) - - cmd.PersistentFlags().StringVar(&cconfig.RedirectURL, "redirect-url", "http://localhost:9876/callback", "client redirect url") - cmd.PersistentFlags().StringVar(&cconfig.ClientID, "client-id", "", "client identifier") - cmd.PersistentFlags().StringVar(&cconfig.ClientSecret, "client-secret", "", "client secret") - cmd.PersistentFlags().StringVar(&cconfig.GrantType, "grant-type", "", "grant type") - cmd.PersistentFlags().StringVar(&cconfig.AuthMethod, "auth-method", "", "token endpoint authentication method") - cmd.PersistentFlags().StringVar(&cconfig.Username, "username", "", "resource owner password credentials grant flow username") - cmd.PersistentFlags().StringVar(&cconfig.Password, "password", "", "resource owner password credentials grant flow password") - cmd.PersistentFlags().StringVar(&cconfig.RefreshToken, "refresh-token", "", "refresh token") - cmd.PersistentFlags().StringSliceVar(&cconfig.ResponseType, "response-types", []string{""}, "response type") - cmd.PersistentFlags().StringVar(&cconfig.ResponseMode, "response-mode", "", "response mode") - cmd.PersistentFlags().StringSliceVar(&cconfig.Scopes, "scopes", []string{}, "requested scopes") - cmd.PersistentFlags().StringSliceVar(&cconfig.Audience, "audience", []string{}, "requested audience") - cmd.PersistentFlags().BoolVar(&cconfig.PKCE, "pkce", false, "enable proof key for code exchange (PKCE)") - cmd.PersistentFlags().BoolVar(&cconfig.PAR, "par", false, "enable pushed authorization requests (PAR)") - cmd.PersistentFlags().BoolVar(&cconfig.RequestObject, "request-object", false, "pass request parameters as jwt") - cmd.PersistentFlags().BoolVar(&cconfig.EncryptedRequestObject, "encrypted-request-object", false, "pass request parameters as encrypted jwt") - cmd.PersistentFlags().StringVar(&cconfig.Assertion, "assertion", "", "claims for jwt bearer assertion") - cmd.PersistentFlags().StringVar(&cconfig.SigningKey, "signing-key", "", "path or url to signing key in jwks format") - cmd.PersistentFlags().StringVar(&cconfig.EncryptionKey, "encryption-key", "", "path or url to encryption key in jwks format") - cmd.PersistentFlags().StringVar(&cconfig.SubjectToken, "subject-token", "", "third party token") - cmd.PersistentFlags().StringVar(&cconfig.SubjectTokenType, "subject-token-type", "", "third party token type") - cmd.PersistentFlags().StringVar(&cconfig.ActorToken, "actor-token", "", "acting party token") - cmd.PersistentFlags().StringVar(&cconfig.ActorTokenType, "actor-token-type", "", "acting party token type") - cmd.PersistentFlags().StringVar(&cconfig.IDTokenHint, "id-token-hint", "", "id token hint") - cmd.PersistentFlags().StringVar(&cconfig.LoginHint, "login-hint", "", "user identifier hint") - cmd.PersistentFlags().StringVar(&cconfig.IDPHint, "idp-hint", "", "identity provider hint") - cmd.PersistentFlags().StringVar(&cconfig.TLSCert, "tls-cert", "", "path to tls cert pem file") - cmd.PersistentFlags().StringVar(&cconfig.TLSKey, "tls-key", "", "path to tls key pem file") - cmd.PersistentFlags().StringVar(&cconfig.TLSRootCA, "tls-root-ca", "", "path to tls root ca pem file") - cmd.PersistentFlags().DurationVar(&cconfig.HTTPTimeout, "http-timeout", time.Minute, "http client timeout") - cmd.PersistentFlags().DurationVar(&cconfig.BrowserTimeout, "browser-timeout", 10*time.Minute, "browser timeout") - cmd.PersistentFlags().BoolVar(&cconfig.Insecure, "insecure", false, "allow insecure connections") - cmd.PersistentFlags().BoolVarP(&silent, "silent", "s", false, "silent mode") - cmd.PersistentFlags().BoolVar(&noPrompt, "no-prompt", false, "disable prompt") - cmd.PersistentFlags().BoolVar(&cconfig.DPoP, "dpop", false, "use DPoP") - cmd.PersistentFlags().StringVar(&cconfig.Claims, "claims", "", "use claims") - cmd.PersistentFlags().StringVar(&cconfig.RAR, "rar", "", "use rich authorization request (RAR)") - cmd.PersistentFlags().StringSliceVar(&cconfig.ACRValues, "acr-values", []string{}, "ACR values") - cmd.PersistentFlags().StringVar(&cconfig.Purpose, "purpose", "", "string describing the purpose for obtaining End-User authorization") + cmd.AddCommand(jwksCmd) + + cmd.Flags().StringVar(&cconfig.RedirectURL, "redirect-url", "http://localhost:9876/callback", "client redirect url") + cmd.Flags().StringVar(&cconfig.ClientID, "client-id", "", "client identifier") + cmd.Flags().StringVar(&cconfig.ClientSecret, "client-secret", "", "client secret") + cmd.Flags().StringVar(&cconfig.GrantType, "grant-type", "", "grant type") + cmd.Flags().StringVar(&cconfig.AuthMethod, "auth-method", "", "token endpoint authentication method") + cmd.Flags().StringVar(&cconfig.Username, "username", "", "resource owner password credentials grant flow username") + cmd.Flags().StringVar(&cconfig.Password, "password", "", "resource owner password credentials grant flow password") + cmd.Flags().StringVar(&cconfig.RefreshToken, "refresh-token", "", "refresh token") + cmd.Flags().StringSliceVar(&cconfig.ResponseType, "response-types", []string{""}, "response type") + cmd.Flags().StringVar(&cconfig.ResponseMode, "response-mode", "", "response mode") + cmd.Flags().StringSliceVar(&cconfig.Scopes, "scopes", []string{}, "requested scopes") + cmd.Flags().StringSliceVar(&cconfig.Audience, "audience", []string{}, "requested audience") + cmd.Flags().BoolVar(&cconfig.PKCE, "pkce", false, "enable proof key for code exchange (PKCE)") + cmd.Flags().BoolVar(&cconfig.PAR, "par", false, "enable pushed authorization requests (PAR)") + cmd.Flags().BoolVar(&cconfig.RequestObject, "request-object", false, "pass request parameters as jwt") + cmd.Flags().BoolVar(&cconfig.EncryptedRequestObject, "encrypted-request-object", false, "pass request parameters as encrypted jwt") + cmd.Flags().StringVar(&cconfig.Assertion, "assertion", "", "claims for jwt bearer assertion") + cmd.Flags().StringVar(&cconfig.SigningKey, "signing-key", "", "path or url to signing key in jwks format") + cmd.Flags().StringVar(&cconfig.EncryptionKey, "encryption-key", "", "path or url to encryption key in jwks format") + cmd.Flags().StringVar(&cconfig.SubjectToken, "subject-token", "", "third party token") + cmd.Flags().StringVar(&cconfig.SubjectTokenType, "subject-token-type", "", "third party token type") + cmd.Flags().StringVar(&cconfig.ActorToken, "actor-token", "", "acting party token") + cmd.Flags().StringVar(&cconfig.ActorTokenType, "actor-token-type", "", "acting party token type") + cmd.Flags().StringVar(&cconfig.IDTokenHint, "id-token-hint", "", "id token hint") + cmd.Flags().StringVar(&cconfig.LoginHint, "login-hint", "", "user identifier hint") + cmd.Flags().StringVar(&cconfig.IDPHint, "idp-hint", "", "identity provider hint") + cmd.Flags().StringVar(&cconfig.TLSCert, "tls-cert", "", "path to tls cert pem file") + cmd.Flags().StringVar(&cconfig.TLSKey, "tls-key", "", "path to tls key pem file") + cmd.Flags().StringVar(&cconfig.TLSRootCA, "tls-root-ca", "", "path to tls root ca pem file") + cmd.Flags().DurationVar(&cconfig.HTTPTimeout, "http-timeout", time.Minute, "http client timeout") + cmd.Flags().DurationVar(&cconfig.BrowserTimeout, "browser-timeout", 10*time.Minute, "browser timeout") + cmd.Flags().BoolVar(&cconfig.Insecure, "insecure", false, "allow insecure connections") + cmd.Flags().BoolVarP(&silent, "silent", "s", false, "silent mode") + cmd.Flags().BoolVar(&noPrompt, "no-prompt", false, "disable prompt") + cmd.Flags().BoolVar(&cconfig.DPoP, "dpop", false, "use DPoP") + cmd.Flags().StringVar(&cconfig.Claims, "claims", "", "use claims") + cmd.Flags().StringVar(&cconfig.RAR, "rar", "", "use rich authorization request (RAR)") + cmd.Flags().StringSliceVar(&cconfig.ACRValues, "acr-values", []string{}, "ACR values") + cmd.Flags().StringVar(&cconfig.Purpose, "purpose", "", "string describing the purpose for obtaining End-User authorization") return cmd } diff --git a/internal/jwks/jwks.go b/internal/jwks/jwks.go new file mode 100644 index 0000000..3c5da7b --- /dev/null +++ b/internal/jwks/jwks.go @@ -0,0 +1,123 @@ +package jwks + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "fmt" + + "github.com/go-jose/go-jose/v3" + "github.com/google/uuid" +) + +type Config struct { + Type string + Size int + Curve int + Alg string + Use string +} + +func Generate(config Config) (jose.JSONWebKey, error) { + var ( + jwk = jose.JSONWebKey{ + KeyID: uuid.New().String(), + } + err error + ) + + switch config.Use { + case "sig": + jwk.Use = "sig" + + switch config.Type { + case "rsa": + switch config.Alg { + case "RS256", "RS384", "RS512", "PS256", "PS384", "PS512": + jwk.Algorithm = config.Alg + case "": + jwk.Algorithm = "RS256" + default: + return jwk, fmt.Errorf("unknown algorithm: %s (use RS256, RS384, RS512, PS256, PS384, PS512)", config.Alg) + } + + case "ec": + switch config.Alg { + case "ES256", "ES384", "ES512": + jwk.Algorithm = config.Alg + case "": + jwk.Algorithm = "ES256" + default: + return jwk, fmt.Errorf("unknown algorithm: %s (use ES256, ES384 or ES512)", config.Alg) + } + } + + case "enc": + jwk.Use = "enc" + + switch config.Type { + case "rsa": + switch config.Alg { + case "RSA1_5", "RSA-OAEP", "RSA-OAEP-256": + jwk.Algorithm = config.Alg + case "": + jwk.Algorithm = "RSA-OAEP-256" + default: + return jwk, fmt.Errorf("unknown algorithm: %s (use RSA1_5, RSA-OAEP, RSA-OAEP-256)", config.Alg) + } + + case "ec": + switch config.Alg { + case "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW": + jwk.Algorithm = config.Alg + case "": + jwk.Algorithm = "ECDH-ES+A128KW" + } + } + + default: + return jwk, fmt.Errorf("invalid use: %s (use sig or enc)", config.Use) + } + + switch config.Type { + case "rsa": + var size int + + switch config.Size { + case 2048, 3072, 4096: + size = config.Size + default: + return jwk, fmt.Errorf("invalid key size: %d (use 2048, 3072 or 4096)", config.Size) + } + + if jwk.Key, err = rsa.GenerateKey(rand.Reader, size); err != nil { + return jwk, err + } + + case "ec": + var curve elliptic.Curve + + switch config.Curve { + case 224: + curve = elliptic.P224() + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + return jwk, fmt.Errorf("unknown elliptic curve: %d (use 224, 256, 284 or 521)", config.Curve) + } + + if jwk.Key, err = ecdsa.GenerateKey(curve, rand.Reader); err != nil { + return jwk, err + } + + default: + return jwk, fmt.Errorf("uknown key type: %s (use rsa or ec)", config.Type) + } + + return jwk, nil +}