Use pkg/token to generate JWT token

This commit refactors the approach to encode a token in handler of /service/token,
by reusing pkg/token to avoid inconsistency.

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2020-10-14 14:34:35 +08:00
parent 6cdae44dc2
commit fb687aeef8
8 changed files with 137 additions and 99 deletions

View File

@ -27,7 +27,7 @@ if [[ -d /harbor_cust_cert && -n "$(ls -A /harbor_cust_cert)" ]]; then
case ${z} in
*.crt | *.ca | *.ca-bundle | *.pem)
if [ -d "$z" ]; then
echo "$z is dirictory, skip it ..."
echo "$z is directory, skip it ..."
else
cat $z >> /etc/pki/tls/certs/ca-bundle.crt
echo " $z Appended ..."

View File

@ -15,13 +15,10 @@
package token
import (
"crypto"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/docker/distribution/registry/auth/token"
"github.com/docker/libtrust"
"github.com/goharbor/harbor/src/common/models"
@ -30,14 +27,17 @@ import (
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/lib/log"
tokenpkg "github.com/goharbor/harbor/src/pkg/token"
"github.com/goharbor/harbor/src/pkg/token/claims/v2"
)
const (
// Issuer is the issuer of the internal token service in Harbor for registry
Issuer = "harbor-token-issuer"
signingMethod = "RS256"
)
var privateKey string
var (
privateKey string
)
func init() {
privateKey = config.TokenPrivateKeyPath()
@ -56,7 +56,7 @@ func GetResourceActions(scopes []string) []*token.ResourceActions {
typee := ""
name := ""
actions := []string{}
actions := make([]string, 0)
if length == 1 {
typee = items[0]
@ -104,7 +104,7 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context,
// MakeToken makes a valid jwt token based on parms.
func MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) {
pk, err := libtrust.LoadKeyFile(privateKey)
options, err := tokenpkg.NewOptions(signingMethod, v2.Issuer, privateKey)
if err != nil {
return nil, err
}
@ -112,21 +112,45 @@ func MakeToken(username, service string, access []*token.ResourceActions) (*mode
if err != nil {
return nil, err
}
now := time.Now().UTC()
tk, expiresIn, issuedAt, err := makeTokenCore(Issuer, username, service, expiration, access, pk)
claims := &v2.Claims{
StandardClaims: jwt.StandardClaims{
Issuer: options.Issuer,
Subject: username,
Audience: service,
ExpiresAt: now.Add(time.Duration(expiration) * time.Minute).Unix(),
NotBefore: now.Unix(),
IssuedAt: now.Unix(),
Id: utils.GenerateRandomStringWithLen(16),
},
Access: access,
}
tok, err := tokenpkg.New(options, claims)
if err != nil {
return nil, err
}
// Add kid to token header for compatibility with docker distribution's code
// see https://github.com/docker/distribution/blob/release/2.7/registry/auth/token/token.go#L197
k, err := libtrust.UnmarshalPrivateKeyPEM(options.PrivateKey)
if err != nil {
return nil, err
}
tok.Header["kid"] = k.KeyID()
rawToken, err := tok.Raw()
if err != nil {
return nil, err
}
rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature))
return &models.Token{
Token: rs,
ExpiresIn: expiresIn,
IssuedAt: issuedAt.Format(time.RFC3339),
Token: rawToken,
ExpiresIn: expiration * 60,
IssuedAt: now.Format(time.RFC3339),
}, nil
}
func permToActions(p string) []string {
res := []string{}
res := make([]string, 0)
if strings.Contains(p, "W") {
res = append(res, "push")
}
@ -141,58 +165,3 @@ func permToActions(p string) []string {
}
return res
}
// make token core
func makeTokenCore(issuer, subject, audience string, expiration int,
access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) {
joseHeader := &token.Header{
Type: "JWT",
SigningAlg: "RS256",
KeyID: signingKey.KeyID(),
}
jwtID := utils.GenerateRandomStringWithLen(16)
now := time.Now().UTC()
issuedAt = &now
expiresIn = expiration * 60
claimSet := &token.ClaimSet{
Issuer: issuer,
Subject: subject,
Audience: audience,
Expiration: now.Add(time.Duration(expiration) * time.Minute).Unix(),
NotBefore: now.Unix(),
IssuedAt: now.Unix(),
JWTID: jwtID,
Access: access,
}
var joseHeaderBytes, claimSetBytes []byte
if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
return nil, 0, nil, fmt.Errorf("unable to marshal jose header: %s", err)
}
if claimSetBytes, err = json.Marshal(claimSet); err != nil {
return nil, 0, nil, fmt.Errorf("unable to marshal claim set: %s", err)
}
encodedJoseHeader := base64UrlEncode(joseHeaderBytes)
encodedClaimSet := base64UrlEncode(claimSetBytes)
payload := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)
var signatureBytes []byte
if signatureBytes, _, err = signingKey.Sign(strings.NewReader(payload), crypto.SHA256); err != nil {
return nil, 0, nil, fmt.Errorf("unable to sign jwt payload: %s", err)
}
signature := base64UrlEncode(signatureBytes)
tokenString := fmt.Sprintf("%s.%s", payload, signature)
t, err = token.NewToken(tokenString)
return
}
func base64UrlEncode(b []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
}

View File

@ -136,14 +136,13 @@ func TestMakeToken(t *testing.T) {
t.Errorf("Error while making token: %v", err)
}
tokenString := tokenJSON.Token
// t.Logf("privatekey: %s, crt: %s", tokenString, crt)
pubKey, err := getPublicKey(crt)
if err != nil {
t.Errorf("Error while getting public key from cert: %s", crt)
}
tok, err := jwt.ParseWithClaims(tokenString, &harborClaims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return pubKey, nil
})
@ -162,7 +161,7 @@ func TestPermToActions(t *testing.T) {
perm3 := ""
expect1 := []string{"push", "*", "pull"}
expect2 := []string{"*", "pull"}
expect3 := []string{}
expect3 := make([]string, 0)
res1 := permToActions(perm1)
res2 := permToActions(perm2)
res3 := permToActions(perm3)

View File

@ -0,0 +1,30 @@
package v2
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/docker/distribution/registry/auth/token"
)
const (
// Issuer is the only valid issuer for jwt token sent to /v2/xxxx
Issuer = "harbor-token-issuer"
)
// Claims represents the token claims that encapsulated in a JWT token for registry/notary resources
type Claims struct {
jwt.StandardClaims
Access []*token.ResourceActions `json:"access"`
}
// Valid checks if the issuer is harbor
func (c *Claims) Valid() error {
if err := c.StandardClaims.Valid(); err != nil {
return err
}
if !c.VerifyIssuer(Issuer, true) {
return fmt.Errorf("invalid token issuer: %s", c.Issuer)
}
return nil
}

View File

@ -0,0 +1,43 @@
package v2
import (
"testing"
"github.com/dgrijalva/jwt-go"
"github.com/docker/distribution/registry/auth/token"
"github.com/stretchr/testify/assert"
)
func TestValid(t *testing.T) {
cases := []struct {
claims Claims
valid bool
}{
{
claims: Claims{
StandardClaims: jwt.StandardClaims{
Issuer: "anonymous",
},
Access: []*token.ResourceActions{},
},
valid: false,
},
{
claims: Claims{
StandardClaims: jwt.StandardClaims{
Issuer: Issuer,
},
Access: []*token.ResourceActions{},
},
valid: true,
},
}
for _, tc := range cases {
if tc.valid {
assert.Nil(t, tc.claims.Valid())
} else {
assert.NotNil(t, tc.claims.Valid())
}
}
}

View File

@ -1,10 +1,10 @@
package token
import (
"testing"
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewOptions(t *testing.T) {
@ -12,7 +12,6 @@ func TestNewOptions(t *testing.T) {
assert.NotNil(t, defaultOpt)
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
assert.Equal(t, defaultOpt.Issuer, "harbor-token-defaultIssuer")
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
}
func TestGetKey(t *testing.T) {

View File

@ -3,15 +3,14 @@ package token
import (
"crypto/rsa"
"fmt"
"io/ioutil"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log"
"io/ioutil"
"time"
)
const (
defaultTTL = 60 * time.Minute
defaultIssuer = "harbor-token-defaultIssuer"
defaultSignedMethod = "RS256"
)
@ -21,7 +20,6 @@ type Options struct {
SignMethod jwt.SigningMethod
PublicKey []byte
PrivateKey []byte
TTL time.Duration
Issuer string
}
@ -62,17 +60,20 @@ func (o *Options) GetKey() (interface{}, error) {
// DefaultTokenOptions ...
func DefaultTokenOptions() *Options {
privateKeyFile := config.TokenPrivateKeyPath()
privateKey, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
log.Errorf(fmt.Sprintf("failed to read private key %v", err))
return nil
}
opt := &Options{
SignMethod: jwt.GetSigningMethod(defaultSignedMethod),
PrivateKey: privateKey,
Issuer: defaultIssuer,
TTL: defaultTTL,
}
opt, _ := NewOptions(defaultSignedMethod, defaultIssuer, config.TokenPrivateKeyPath())
return opt
}
// NewOptions create Options based on input parms
func NewOptions(sm, iss, keyPath string) (*Options, error) {
pk, err := ioutil.ReadFile(keyPath)
if err != nil {
log.Errorf(fmt.Sprintf("failed to read private key %v", err))
return nil, err
}
return &Options{
PrivateKey: pk,
SignMethod: jwt.GetSigningMethod(sm),
Issuer: iss,
}, nil
}

View File

@ -5,30 +5,27 @@ import (
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
registry_token "github.com/docker/distribution/registry/auth/token"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/v2token"
svc_token "github.com/goharbor/harbor/src/core/service/token"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/token"
v2 "github.com/goharbor/harbor/src/pkg/token/claims/v2"
)
type v2TokenClaims struct {
jwt.StandardClaims
v2.Claims
Access []*registry_token.ResourceActions `json:"access"`
}
func (vtc *v2TokenClaims) Valid() error {
if err := vtc.StandardClaims.Valid(); err != nil {
if err := vtc.Claims.Valid(); err != nil {
return err
}
if !vtc.VerifyAudience(svc_token.Registry, true) {
return fmt.Errorf("invalid token audience: %s", vtc.Audience)
}
if !vtc.VerifyIssuer(svc_token.Issuer, true) {
return fmt.Errorf("invalid token issuer: %s", vtc.Issuer)
}
return nil
}