mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-25 11:46:43 +01:00
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:
parent
6cdae44dc2
commit
fb687aeef8
@ -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 ..."
|
||||
|
@ -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), "=")
|
||||
}
|
||||
|
@ -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)
|
||||
|
30
src/pkg/token/claims/v2/claims.go
Normal file
30
src/pkg/token/claims/v2/claims.go
Normal 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
|
||||
}
|
43
src/pkg/token/claims/v2/claims_test.go
Normal file
43
src/pkg/token/claims/v2/claims_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user