Handle new auth options for signature algo & key

This commit is contained in:
Denis Arh
2022-07-14 11:09:26 +02:00
parent e5a50a7191
commit d1d00d7771
7 changed files with 320 additions and 2 deletions
+20
View File
@@ -441,8 +441,22 @@
# Default: <no value>
# AUTH_PASSWORD_SECURITY=<no value>
###############################################################################
# Algoritm to be use for JWT signature.
#
# Supported valus:
# - HS256, HS384, HS512
# - PS256, PS384, PS512,
# - RS256, RS384, RS512
#
# Provide shared secret string for HS256, HS384, HS512 and full private key or path to the file PS* and RS* algorithms.
# Type: string
# Default: HS512
# AUTH_JWT_ALGORITHM=HS512
###############################################################################
# Secret used for signing JWT tokens.
# Value is used only when HS256, HS384 or HS512 algorithm is used.
#
# [IMPORTANT]
# ====
@@ -453,6 +467,12 @@
# Default: <no value>
# AUTH_JWT_SECRET=<no value>
###############################################################################
# Raw private key or absolute or relative path to the file containing one.
# Type: string
# Default: <no value>
# AUTH_JWT_KEY=<no value>
###############################################################################
# Lifetime of the access token. Should be shorter than lifetime of the refresh token.
# Type: time.Duration
+82 -1
View File
@@ -7,11 +7,20 @@ import (
"github.com/cortezaproject/corteza-server/auth/oauth2"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"go.uber.org/zap"
"io/ioutil"
"strings"
)
func (app *CortezaApp) initAuth(ctx context.Context) (err error) {
log := app.Log.Named("auth")
if app.Opt.Auth.DefaultClient != "" {
// default client will help streamline authorization with default clients
app.DefaultAuthClient, err = store.LookupAuthClientByHandle(ctx, app.Store, app.Opt.Auth.DefaultClient)
@@ -36,8 +45,24 @@ func (app *CortezaApp) initAuth(ctx context.Context) (err error) {
return fmt.Errorf("could not set token verifier")
}
// create token signature function from AUTH_ options
// and pass it on to the token issuer
alg, key, err := prepareSignatureFnParams(app.Opt.Auth)
if err != nil {
return fmt.Errorf("could not initialize token signer: %w", err)
}
log.Info(
"initializing JWT and authentication procedures",
zap.Stringer("algoritm", alg),
)
// construct token issuer with algorithm, secrets,
auth.TokenIssuer, err = auth.NewTokenIssuer(
auth.WithSecretSigner(app.Opt.Auth.Secret),
auth.WithSigner(func(t jwt.Token) ([]byte, error) {
return jwt.Sign(t, alg, key)
}),
// @todo implement configurable issuer claim
//auth.WithDefaultIssuer(app.Opt.Auth.TokenClaimIssuer),
auth.WithDefaultExpiration(app.Opt.Auth.AccessTokenLifetime),
@@ -71,4 +96,60 @@ func (app *CortezaApp) initAuth(ctx context.Context) (err error) {
if err != nil {
return fmt.Errorf("could not initialize token issuer: %w", err)
}
return
}
// helper function that loads and/or parses private keys and initializes JWT signer function
// from the given arguments
func prepareSignatureFnParams(opt options.AuthOpt) (alg jwa.SignatureAlgorithm, key any, err error) {
alg = jwa.SignatureAlgorithm(opt.JwtAlgorithm)
switch alg {
case jwa.HS256, jwa.HS384, jwa.HS512:
// expecting secret to be set
if len(opt.Secret) == 0 {
return alg, nil, fmt.Errorf("token secret missing")
}
key = []byte(opt.Secret)
case
jwa.PS256, jwa.PS384, jwa.PS512,
jwa.RS256, jwa.RS384, jwa.RS512:
if len(opt.JwtKey) == 0 {
return alg, nil, fmt.Errorf("token key missing")
}
// if given key dos not begins with "-----BEGIN"
// assume it's path to a file and load contents of that file
if !strings.HasPrefix(opt.JwtKey, "-----BEGIN") {
var (
keyFile = opt.JwtKey
b []byte
)
if b, err = ioutil.ReadFile(keyFile); err != nil {
return alg, nil, fmt.Errorf("could not read key file: %w", err)
}
// overwrite th input and load
opt.JwtKey = string(b)
// recheck contents of the key
if !strings.HasPrefix(opt.JwtKey, "-----BEGIN") {
return alg, nil, fmt.Errorf("file %q does not contain a valid private key", keyFile)
}
}
// generates pem.Private from the kInput string
key, err = jwk.ParseKey([]byte(opt.JwtKey), jwk.WithPEM(true))
case "":
// should be caught by options init procedure and set to default,
// but you never know...
err = fmt.Errorf("token signature algorithm empty or missing")
default:
err = fmt.Errorf("token signature algorithm %q not supported", alg)
}
return
}
+143
View File
@@ -0,0 +1,143 @@
package app
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/lestrrat-go/jwx/jwa"
"github.com/stretchr/testify/require"
"testing"
)
func TestPrepareSignatureFnParams(t *testing.T) {
type (
test struct {
name string
opt options.AuthOpt
err error
}
)
var (
privateKey = string(genKey())
)
tests := []test{
{
name: "empty algo",
opt: options.AuthOpt{JwtKey: "foobar"},
err: fmt.Errorf("token signature algorithm empty or missing"),
},
{
name: "unknown algo",
opt: options.AuthOpt{JwtAlgorithm: "foobar", JwtKey: "foobar"},
err: fmt.Errorf("token signature algorithm \"foobar\" not supported"),
},
{
name: "empty key",
opt: options.AuthOpt{JwtAlgorithm: jwa.HS256.String()},
err: fmt.Errorf("token secret missing"),
},
{
name: "empty key",
opt: options.AuthOpt{JwtAlgorithm: jwa.PS256.String()},
err: fmt.Errorf("token key missing"),
},
{
// "shared secret" string
name: "HS256",
opt: options.AuthOpt{JwtAlgorithm: jwa.HS256.String(), Secret: "test key"},
},
{
// "shared secret" string
name: "HS384",
opt: options.AuthOpt{JwtAlgorithm: jwa.HS384.String(), Secret: "test key"},
},
{
// "shared secret" string
name: "HS512",
opt: options.AuthOpt{JwtAlgorithm: jwa.HS512.String(), Secret: "test key"},
},
{
// requires private key
name: "PS256",
opt: options.AuthOpt{JwtAlgorithm: jwa.PS256.String(), JwtKey: privateKey},
},
{
// requires private key
name: "PS384",
opt: options.AuthOpt{JwtAlgorithm: jwa.PS384.String(), JwtKey: privateKey},
},
{
// requires private key
name: "PS512",
opt: options.AuthOpt{JwtAlgorithm: jwa.PS512.String(), JwtKey: privateKey},
},
{
// requires private key
name: "RS256",
opt: options.AuthOpt{JwtAlgorithm: jwa.RS256.String(), JwtKey: privateKey},
},
{
// requires private key
name: "RS384",
opt: options.AuthOpt{JwtAlgorithm: jwa.RS384.String(), JwtKey: privateKey},
},
{
// requires private key
name: "RS512",
opt: options.AuthOpt{JwtAlgorithm: jwa.RS512.String(), JwtKey: privateKey},
},
{
// requires private key
name: "RS512 from a file",
opt: options.AuthOpt{JwtAlgorithm: jwa.RS512.String(), JwtKey: "test_files/key.pem"},
},
{
// requires private key
name: "RS512 from a non-existing file",
opt: options.AuthOpt{JwtAlgorithm: jwa.RS512.String(), JwtKey: "test_files/not-here.pem"},
err: fmt.Errorf("could not read key file: open test_files/not-here.pem: no such file or directory"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
req = require.New(t)
)
alg, key, err := prepareSignatureFnParams(tt.opt)
if tt.err != nil {
req.EqualError(err, tt.err.Error())
return
}
req.NoError(err)
req.NotEmpty(alg)
req.NotEmpty(key)
})
}
}
func genKey() []byte {
bitSize := 4096
// Generate RSA key.
key, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
panic(err)
}
// Encode private key to PKCS#1 ASN.1 PEM.
return pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
},
)
}
+1 -1
View File
@@ -289,7 +289,7 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
}
if err = app.initAuth(ctx); err != nil {
return
return fmt.Errorf("can not initialize auth: %w", err)
}
app.WsServer = websocket.Server(
+20
View File
@@ -29,10 +29,25 @@ auth: schema.#optionsGroup & {
====
"""
}
jwt_algorithm: {
defaultGoExpr: "\"HS512\""
defaultValue: "HS512"
description: """
Algoritm to be use for JWT signature.
Supported valus:
- HS256, HS384, HS512
- PS256, PS384, PS512,
- RS256, RS384, RS512
Provide shared secret string for HS256, HS384, HS512 and full private key or path to the file PS* and RS* algorithms.
"""
}
secret: {
defaultGoExpr: "getSecretFromEnv(\"jwt secret\")"
description: """
Secret used for signing JWT tokens.
Value is used only when HS256, HS384 or HS512 algorithm is used.
[IMPORTANT]
====
@@ -42,6 +57,11 @@ auth: schema.#optionsGroup & {
"""
env: "AUTH_JWT_SECRET"
}
jwt_key: {
description: """
Raw private key or absolute or relative path to the file containing one.
"""
}
access_token_lifetime: {
type: "time.Duration"
description: """
+51
View File
@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
+3
View File
@@ -97,7 +97,9 @@ type (
AuthOpt struct {
LogEnabled bool `env:"AUTH_LOG_ENABLED"`
PasswordSecurity bool `env:"AUTH_PASSWORD_SECURITY"`
JwtAlgorithm string `env:"AUTH_JWT_ALGORITHM"`
Secret string `env:"AUTH_JWT_SECRET"`
JwtKey string `env:"AUTH_JWT_KEY"`
AccessTokenLifetime time.Duration `env:"AUTH_OAUTH2_ACCESS_TOKEN_LIFETIME"`
RefreshTokenLifetime time.Duration `env:"AUTH_OAUTH2_REFRESH_TOKEN_LIFETIME"`
ExternalRedirectURL string `env:"AUTH_EXTERNAL_REDIRECT_URL"`
@@ -514,6 +516,7 @@ func Apigw() (o *ApigwOpt) {
func Auth() (o *AuthOpt) {
o = &AuthOpt{
PasswordSecurity: true,
JwtAlgorithm: "HS512",
Secret: getSecretFromEnv("jwt secret"),
AccessTokenLifetime: time.Hour * 2,
RefreshTokenLifetime: time.Hour * 24 * 3,