mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-28 05:18:01 +02: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
|
case ${z} in
|
||||||
*.crt | *.ca | *.ca-bundle | *.pem)
|
*.crt | *.ca | *.ca-bundle | *.pem)
|
||||||
if [ -d "$z" ]; then
|
if [ -d "$z" ]; then
|
||||||
echo "$z is dirictory, skip it ..."
|
echo "$z is directory, skip it ..."
|
||||||
else
|
else
|
||||||
cat $z >> /etc/pki/tls/certs/ca-bundle.crt
|
cat $z >> /etc/pki/tls/certs/ca-bundle.crt
|
||||||
echo " $z Appended ..."
|
echo " $z Appended ..."
|
||||||
|
@ -15,13 +15,10 @@
|
|||||||
package token
|
package token
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/docker/distribution/registry/auth/token"
|
"github.com/docker/distribution/registry/auth/token"
|
||||||
"github.com/docker/libtrust"
|
"github.com/docker/libtrust"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"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/config"
|
||||||
"github.com/goharbor/harbor/src/core/promgr"
|
"github.com/goharbor/harbor/src/core/promgr"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"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 (
|
const (
|
||||||
// Issuer is the issuer of the internal token service in Harbor for registry
|
signingMethod = "RS256"
|
||||||
Issuer = "harbor-token-issuer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var privateKey string
|
var (
|
||||||
|
privateKey string
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
privateKey = config.TokenPrivateKeyPath()
|
privateKey = config.TokenPrivateKeyPath()
|
||||||
@ -56,7 +56,7 @@ func GetResourceActions(scopes []string) []*token.ResourceActions {
|
|||||||
|
|
||||||
typee := ""
|
typee := ""
|
||||||
name := ""
|
name := ""
|
||||||
actions := []string{}
|
actions := make([]string, 0)
|
||||||
|
|
||||||
if length == 1 {
|
if length == 1 {
|
||||||
typee = items[0]
|
typee = items[0]
|
||||||
@ -104,7 +104,7 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context,
|
|||||||
|
|
||||||
// MakeToken makes a valid jwt token based on parms.
|
// MakeToken makes a valid jwt token based on parms.
|
||||||
func MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -112,21 +112,45 @@ func MakeToken(username, service string, access []*token.ResourceActions) (*mode
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature))
|
|
||||||
return &models.Token{
|
return &models.Token{
|
||||||
Token: rs,
|
Token: rawToken,
|
||||||
ExpiresIn: expiresIn,
|
ExpiresIn: expiration * 60,
|
||||||
IssuedAt: issuedAt.Format(time.RFC3339),
|
IssuedAt: now.Format(time.RFC3339),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func permToActions(p string) []string {
|
func permToActions(p string) []string {
|
||||||
res := []string{}
|
res := make([]string, 0)
|
||||||
if strings.Contains(p, "W") {
|
if strings.Contains(p, "W") {
|
||||||
res = append(res, "push")
|
res = append(res, "push")
|
||||||
}
|
}
|
||||||
@ -141,58 +165,3 @@ func permToActions(p string) []string {
|
|||||||
}
|
}
|
||||||
return res
|
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)
|
t.Errorf("Error while making token: %v", err)
|
||||||
}
|
}
|
||||||
tokenString := tokenJSON.Token
|
tokenString := tokenJSON.Token
|
||||||
// t.Logf("privatekey: %s, crt: %s", tokenString, crt)
|
|
||||||
pubKey, err := getPublicKey(crt)
|
pubKey, err := getPublicKey(crt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while getting public key from cert: %s", crt)
|
t.Errorf("Error while getting public key from cert: %s", crt)
|
||||||
}
|
}
|
||||||
tok, err := jwt.ParseWithClaims(tokenString, &harborClaims{}, func(t *jwt.Token) (interface{}, error) {
|
tok, err := jwt.ParseWithClaims(tokenString, &harborClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
|
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
|
return pubKey, nil
|
||||||
})
|
})
|
||||||
@ -162,7 +161,7 @@ func TestPermToActions(t *testing.T) {
|
|||||||
perm3 := ""
|
perm3 := ""
|
||||||
expect1 := []string{"push", "*", "pull"}
|
expect1 := []string{"push", "*", "pull"}
|
||||||
expect2 := []string{"*", "pull"}
|
expect2 := []string{"*", "pull"}
|
||||||
expect3 := []string{}
|
expect3 := make([]string, 0)
|
||||||
res1 := permToActions(perm1)
|
res1 := permToActions(perm1)
|
||||||
res2 := permToActions(perm2)
|
res2 := permToActions(perm2)
|
||||||
res3 := permToActions(perm3)
|
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
|
package token
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewOptions(t *testing.T) {
|
func TestNewOptions(t *testing.T) {
|
||||||
@ -12,7 +12,6 @@ func TestNewOptions(t *testing.T) {
|
|||||||
assert.NotNil(t, defaultOpt)
|
assert.NotNil(t, defaultOpt)
|
||||||
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
|
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
|
||||||
assert.Equal(t, defaultOpt.Issuer, "harbor-token-defaultIssuer")
|
assert.Equal(t, defaultOpt.Issuer, "harbor-token-defaultIssuer")
|
||||||
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetKey(t *testing.T) {
|
func TestGetKey(t *testing.T) {
|
||||||
|
@ -3,15 +3,14 @@ package token
|
|||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultTTL = 60 * time.Minute
|
|
||||||
defaultIssuer = "harbor-token-defaultIssuer"
|
defaultIssuer = "harbor-token-defaultIssuer"
|
||||||
defaultSignedMethod = "RS256"
|
defaultSignedMethod = "RS256"
|
||||||
)
|
)
|
||||||
@ -21,7 +20,6 @@ type Options struct {
|
|||||||
SignMethod jwt.SigningMethod
|
SignMethod jwt.SigningMethod
|
||||||
PublicKey []byte
|
PublicKey []byte
|
||||||
PrivateKey []byte
|
PrivateKey []byte
|
||||||
TTL time.Duration
|
|
||||||
Issuer string
|
Issuer string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,17 +60,20 @@ func (o *Options) GetKey() (interface{}, error) {
|
|||||||
|
|
||||||
// DefaultTokenOptions ...
|
// DefaultTokenOptions ...
|
||||||
func DefaultTokenOptions() *Options {
|
func DefaultTokenOptions() *Options {
|
||||||
privateKeyFile := config.TokenPrivateKeyPath()
|
opt, _ := NewOptions(defaultSignedMethod, defaultIssuer, 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,
|
|
||||||
}
|
|
||||||
return opt
|
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"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
registry_token "github.com/docker/distribution/registry/auth/token"
|
registry_token "github.com/docker/distribution/registry/auth/token"
|
||||||
"github.com/goharbor/harbor/src/common/security"
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
"github.com/goharbor/harbor/src/common/security/v2token"
|
"github.com/goharbor/harbor/src/common/security/v2token"
|
||||||
svc_token "github.com/goharbor/harbor/src/core/service/token"
|
svc_token "github.com/goharbor/harbor/src/core/service/token"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/token"
|
"github.com/goharbor/harbor/src/pkg/token"
|
||||||
|
v2 "github.com/goharbor/harbor/src/pkg/token/claims/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type v2TokenClaims struct {
|
type v2TokenClaims struct {
|
||||||
jwt.StandardClaims
|
v2.Claims
|
||||||
Access []*registry_token.ResourceActions `json:"access"`
|
Access []*registry_token.ResourceActions `json:"access"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vtc *v2TokenClaims) Valid() error {
|
func (vtc *v2TokenClaims) Valid() error {
|
||||||
if err := vtc.StandardClaims.Valid(); err != nil {
|
if err := vtc.Claims.Valid(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !vtc.VerifyAudience(svc_token.Registry, true) {
|
if !vtc.VerifyAudience(svc_token.Registry, true) {
|
||||||
return fmt.Errorf("invalid token audience: %s", vtc.Audience)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user