Handle new auth options for signature algo & key
This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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: """
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Generated
+3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user