forked from TrueCloudLab/distribution
Merge pull request #859 from jlhawn/ng_auth_package
Adds auth package with token auth backend
This commit is contained in:
commit
3c0dbe2595
6 changed files with 1157 additions and 0 deletions
107
auth/auth.go
Normal file
107
auth/auth.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// Package auth defines a standard interface for request access controllers.
|
||||||
|
//
|
||||||
|
// An access controller has a simple interface with a single `Authorized`
|
||||||
|
// method which checks that a given request is authorized to perform one or
|
||||||
|
// more actions on one or more resources. This method should return a non-nil
|
||||||
|
// error if the requset is not authorized.
|
||||||
|
//
|
||||||
|
// An implementation registers its access controller by name with a constructor
|
||||||
|
// which accepts an options map for configuring the access controller.
|
||||||
|
//
|
||||||
|
// options := map[string]interface{}{"sillySecret": "whysosilly?"}
|
||||||
|
// accessController, _ := auth.GetAccessController("silly", options)
|
||||||
|
//
|
||||||
|
// This `accessController` can then be used in a request handler like so:
|
||||||
|
//
|
||||||
|
// func updateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// orderNumber := r.FormValue("orderNumber")
|
||||||
|
// resource := auth.Resource{Type: "customerOrder", Name: orderNumber}
|
||||||
|
// access := auth.Access{Resource: resource, Action: "update"}
|
||||||
|
//
|
||||||
|
// if err := accessController.Authorized(r, access); err != nil {
|
||||||
|
// if challenge, ok := err.(auth.Challenge) {
|
||||||
|
// // Let the challenge write the response.
|
||||||
|
// challenge.ServeHTTP(w, r)
|
||||||
|
// } else {
|
||||||
|
// // Some other error.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource describes a resource by type and name.
|
||||||
|
type Resource struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access describes a specific action that is
|
||||||
|
// requested or allowed for a given recource.
|
||||||
|
type Access struct {
|
||||||
|
Resource
|
||||||
|
Action string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Challenge is a special error type which is used for HTTP 401 Unauthorized
|
||||||
|
// responses and is able to write the response with WWW-Authenticate challenge
|
||||||
|
// header values based on the error.
|
||||||
|
type Challenge interface {
|
||||||
|
error
|
||||||
|
// ServeHTTP prepares the request to conduct the appropriate challenge
|
||||||
|
// response. For most implementations, simply calling ServeHTTP should be
|
||||||
|
// sufficient. Because no body is written, users may write a custom body after
|
||||||
|
// calling ServeHTTP, but any headers must be written before the call and may
|
||||||
|
// be overwritten.
|
||||||
|
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessController controls access to registry resources based on a request
|
||||||
|
// and required access levels for a request. Implementations can support both
|
||||||
|
// complete denial and http authorization challenges.
|
||||||
|
type AccessController interface {
|
||||||
|
// Authorized returns non-nil if the request is granted access. If one or
|
||||||
|
// more Access structs are provided, the requested access will be compared
|
||||||
|
// with what is available to the request. If the error is non-nil, access
|
||||||
|
// should always be denied. The error may be of type Challenge, in which
|
||||||
|
// case the caller may have the Challenge handle the request or choose
|
||||||
|
// what action to take based on the Challenge header or response status.
|
||||||
|
Authorized(req *http.Request, access ...Access) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitFunc is the type of an AccessController factory function and is used
|
||||||
|
// to register the contsructor for different AccesController backends.
|
||||||
|
type InitFunc func(options map[string]interface{}) (AccessController, error)
|
||||||
|
|
||||||
|
var accessControllers map[string]InitFunc
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
accessControllers = make(map[string]InitFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register is used to register an InitFunc for
|
||||||
|
// an AccessController backend with the given name.
|
||||||
|
func Register(name string, initFunc InitFunc) error {
|
||||||
|
if _, exists := accessControllers[name]; exists {
|
||||||
|
return fmt.Errorf("name already registered: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessControllers[name] = initFunc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessController constructs an AccessController
|
||||||
|
// with the given options using the named backend.
|
||||||
|
func GetAccessController(name string, options map[string]interface{}) (AccessController, error) {
|
||||||
|
if initFunc, exists := accessControllers[name]; exists {
|
||||||
|
return initFunc(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no access controller registered with name: %s", name)
|
||||||
|
}
|
270
auth/token/accesscontroller.go
Normal file
270
auth/token/accesscontroller.go
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/libtrust"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/auth"
|
||||||
|
"github.com/docker/docker-registry/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// accessSet maps a typed, named resource to
|
||||||
|
// a set of actions requested or authorized.
|
||||||
|
type accessSet map[auth.Resource]actionSet
|
||||||
|
|
||||||
|
// newAccessSet constructs an accessSet from
|
||||||
|
// a variable number of auth.Access items.
|
||||||
|
func newAccessSet(accessItems ...auth.Access) accessSet {
|
||||||
|
accessSet := make(accessSet, len(accessItems))
|
||||||
|
|
||||||
|
for _, access := range accessItems {
|
||||||
|
resource := auth.Resource{
|
||||||
|
Type: access.Type,
|
||||||
|
Name: access.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
set, exists := accessSet[resource]
|
||||||
|
if !exists {
|
||||||
|
set = newActionSet()
|
||||||
|
accessSet[resource] = set
|
||||||
|
}
|
||||||
|
|
||||||
|
set.Add(access.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains returns whether or not the given access is in this accessSet.
|
||||||
|
func (s accessSet) contains(access auth.Access) bool {
|
||||||
|
actionSet, ok := s[access.Resource]
|
||||||
|
if ok {
|
||||||
|
return actionSet.Contains(access.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// scopeParam returns a collection of scopes which can
|
||||||
|
// be used for a WWW-Authenticate challenge parameter.
|
||||||
|
// See https://tools.ietf.org/html/rfc6750#section-3
|
||||||
|
func (s accessSet) scopeParam() string {
|
||||||
|
scopes := make([]string, 0, len(s))
|
||||||
|
|
||||||
|
for resource, actionSet := range s {
|
||||||
|
actions := strings.Join(actionSet.Keys(), ",")
|
||||||
|
scopes = append(scopes, fmt.Sprintf("%s:%s:%s", resource.Type, resource.Name, actions))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(scopes, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors used and exported by this package.
|
||||||
|
var (
|
||||||
|
ErrInsufficientScope = errors.New("insufficient scope")
|
||||||
|
ErrTokenRequired = errors.New("authorization token required")
|
||||||
|
)
|
||||||
|
|
||||||
|
// authChallenge implements the auth.Challenge interface.
|
||||||
|
type authChallenge struct {
|
||||||
|
err error
|
||||||
|
realm string
|
||||||
|
service string
|
||||||
|
accessSet accessSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the internal error string for this authChallenge.
|
||||||
|
func (ac *authChallenge) Error() string {
|
||||||
|
return ac.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns the HTTP Response Status Code for this authChallenge.
|
||||||
|
func (ac *authChallenge) Status() int {
|
||||||
|
return http.StatusUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
// challengeParams constructs the value to be used in
|
||||||
|
// the WWW-Authenticate response challenge header.
|
||||||
|
// See https://tools.ietf.org/html/rfc6750#section-3
|
||||||
|
func (ac *authChallenge) challengeParams() string {
|
||||||
|
str := fmt.Sprintf("Bearer realm=%s,service=%s", strconv.Quote(ac.realm), strconv.Quote(ac.service))
|
||||||
|
|
||||||
|
if scope := ac.accessSet.scopeParam(); scope != "" {
|
||||||
|
str = fmt.Sprintf("%s,scope=%s", str, strconv.Quote(scope))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken {
|
||||||
|
str = fmt.Sprintf("%s,error=%s", str, strconv.Quote("invalid_token"))
|
||||||
|
} else if ac.err == ErrInsufficientScope {
|
||||||
|
str = fmt.Sprintf("%s,error=%s", str, strconv.Quote("insufficient_scope"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader sets the WWW-Authenticate value for the given header.
|
||||||
|
func (ac *authChallenge) SetHeader(header http.Header) {
|
||||||
|
header.Add("WWW-Authenticate", ac.challengeParams())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHttp handles writing the challenge response
|
||||||
|
// by setting the challenge header and status code.
|
||||||
|
func (ac *authChallenge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ac.SetHeader(w.Header())
|
||||||
|
w.WriteHeader(ac.Status())
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessController implements the auth.AccessController interface.
|
||||||
|
type accessController struct {
|
||||||
|
realm string
|
||||||
|
issuer string
|
||||||
|
service string
|
||||||
|
rootCerts *x509.CertPool
|
||||||
|
trustedKeys map[string]libtrust.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenAccessOptions is a convenience type for handling
|
||||||
|
// options to the contstructor of an accessController.
|
||||||
|
type tokenAccessOptions struct {
|
||||||
|
realm string
|
||||||
|
issuer string
|
||||||
|
service string
|
||||||
|
rootCertBundle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOptions gathers the necessary options
|
||||||
|
// for an accessController from the given map.
|
||||||
|
func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
|
||||||
|
var opts tokenAccessOptions
|
||||||
|
|
||||||
|
keys := []string{"realm", "issuer", "service", "rootCertBundle"}
|
||||||
|
vals := make([]string, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
val, ok := options[key].(string)
|
||||||
|
if !ok {
|
||||||
|
return opts, fmt.Errorf("token auth requires a valid option string: %q", key)
|
||||||
|
}
|
||||||
|
vals = append(vals, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3]
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAccessController creates an accessController using the given options.
|
||||||
|
func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
|
||||||
|
config, err := checkOptions(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fp, err := os.Open(config.rootCertBundle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
rawCertBundle, err := ioutil.ReadAll(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootCerts []*x509.Certificate
|
||||||
|
pemBlock, rawCertBundle := pem.Decode(rawCertBundle)
|
||||||
|
for pemBlock != nil {
|
||||||
|
cert, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCerts = append(rootCerts, cert)
|
||||||
|
|
||||||
|
pemBlock, rawCertBundle = pem.Decode(rawCertBundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rootCerts) == 0 {
|
||||||
|
return nil, errors.New("token auth requires at least one token signing root certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPool := x509.NewCertPool()
|
||||||
|
trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts))
|
||||||
|
for _, rootCert := range rootCerts {
|
||||||
|
rootPool.AddCert(rootCert)
|
||||||
|
pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err)
|
||||||
|
}
|
||||||
|
trustedKeys[pubKey.KeyID()] = pubKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return &accessController{
|
||||||
|
realm: config.realm,
|
||||||
|
issuer: config.issuer,
|
||||||
|
service: config.service,
|
||||||
|
rootCerts: rootPool,
|
||||||
|
trustedKeys: trustedKeys,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorized handles checking whether the given request is authorized
|
||||||
|
// for actions on resources described by the given access items.
|
||||||
|
func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Access) error {
|
||||||
|
challenge := &authChallenge{
|
||||||
|
realm: ac.realm,
|
||||||
|
service: ac.service,
|
||||||
|
accessSet: newAccessSet(accessItems...),
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(req.Header.Get("Authorization"), " ")
|
||||||
|
|
||||||
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||||
|
challenge.err = ErrTokenRequired
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
rawToken := parts[1]
|
||||||
|
|
||||||
|
token, err := NewToken(rawToken)
|
||||||
|
if err != nil {
|
||||||
|
challenge.err = err
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyOpts := VerifyOptions{
|
||||||
|
TrustedIssuers: common.NewStringSet(ac.issuer),
|
||||||
|
AcceptedAudiences: common.NewStringSet(ac.service),
|
||||||
|
Roots: ac.rootCerts,
|
||||||
|
TrustedKeys: ac.trustedKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = token.Verify(verifyOpts); err != nil {
|
||||||
|
challenge.err = err
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
accessSet := token.accessSet()
|
||||||
|
for _, access := range accessItems {
|
||||||
|
if !accessSet.contains(access) {
|
||||||
|
challenge.err = ErrInsufficientScope
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init handles registering the token auth backend.
|
||||||
|
func init() {
|
||||||
|
auth.Register("token", auth.InitFunc(newAccessController))
|
||||||
|
}
|
332
auth/token/token.go
Normal file
332
auth/token/token.go
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/libtrust"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/auth"
|
||||||
|
"github.com/docker/docker-registry/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TokenSeparator is the value which separates the header, claims, and
|
||||||
|
// signature in the compact serialization of a JSON Web Token.
|
||||||
|
TokenSeparator = "."
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errors used by token parsing and verification.
|
||||||
|
var (
|
||||||
|
ErrMalformedToken = errors.New("malformed token")
|
||||||
|
ErrInvalidToken = errors.New("invalid token")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceActions stores allowed actions on a named and typed resource.
|
||||||
|
type ResourceActions struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Actions []string `json:"actions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimSet describes the main section of a JSON Web Token.
|
||||||
|
type ClaimSet struct {
|
||||||
|
// Public claims
|
||||||
|
Issuer string `json:"iss"`
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Audience string `json:"aud"`
|
||||||
|
Expiration int64 `json:"exp"`
|
||||||
|
NotBefore int64 `json:"nbf"`
|
||||||
|
IssuedAt int64 `json:"iat"`
|
||||||
|
JWTID string `json:"jti"`
|
||||||
|
|
||||||
|
// Private claims
|
||||||
|
Access []*ResourceActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header describes the header section of a JSON Web Token.
|
||||||
|
type Header struct {
|
||||||
|
Type string `json:"typ"`
|
||||||
|
SigningAlg string `json:"alg"`
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
RawJWK json.RawMessage `json:"jwk"`
|
||||||
|
SigningKey libtrust.PublicKey `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckSigningKey parses the `jwk` field of a JOSE header and sets the
|
||||||
|
// SigningKey field if it is valid.
|
||||||
|
func (h *Header) CheckSigningKey() (err error) {
|
||||||
|
if len(h.RawJWK) == 0 {
|
||||||
|
// No signing key was specified.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.SigningKey, err = libtrust.UnmarshalPublicKeyJWK([]byte(h.RawJWK))
|
||||||
|
h.RawJWK = nil // Don't need this anymore!
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token describes a JSON Web Token.
|
||||||
|
type Token struct {
|
||||||
|
Raw string
|
||||||
|
Header *Header
|
||||||
|
Claims *ClaimSet
|
||||||
|
Signature []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyOptions is used to specify
|
||||||
|
// options when verifying a JSON Web Token.
|
||||||
|
type VerifyOptions struct {
|
||||||
|
TrustedIssuers common.StringSet
|
||||||
|
AcceptedAudiences common.StringSet
|
||||||
|
Roots *x509.CertPool
|
||||||
|
TrustedKeys map[string]libtrust.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewToken parses the given raw token string
|
||||||
|
// and constructs an unverified JSON Web Token.
|
||||||
|
func NewToken(rawToken string) (*Token, error) {
|
||||||
|
parts := strings.Split(rawToken, TokenSeparator)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
rawHeader, rawClaims = parts[0], parts[1]
|
||||||
|
headerJSON, claimsJSON []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while unmarshalling raw token: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if headerJSON, err = joseBase64UrlDecode(rawHeader); err != nil {
|
||||||
|
err = fmt.Errorf("unable to decode header: %s", err)
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if claimsJSON, err = joseBase64UrlDecode(rawClaims); err != nil {
|
||||||
|
err = fmt.Errorf("unable to decode claims: %s", err)
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
token := new(Token)
|
||||||
|
token.Header = new(Header)
|
||||||
|
token.Claims = new(ClaimSet)
|
||||||
|
|
||||||
|
token.Raw = strings.Join(parts[:2], TokenSeparator)
|
||||||
|
if token.Signature, err = joseBase64UrlDecode(parts[2]); err != nil {
|
||||||
|
err = fmt.Errorf("unable to decode signature: %s", err)
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(headerJSON, token.Header); err != nil {
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = token.Header.CheckSigningKey(); err != nil {
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(claimsJSON, token.Claims); err != nil {
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify attempts to verify this token using the given options.
|
||||||
|
// Returns a nil error if the token is valid.
|
||||||
|
func (t *Token) Verify(verifyOpts VerifyOptions) error {
|
||||||
|
// Verify that the Issuer claim is a trusted authority.
|
||||||
|
if !verifyOpts.TrustedIssuers.Contains(t.Claims.Issuer) {
|
||||||
|
log.Errorf("token from untrusted issuer: %q", t.Claims.Issuer)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the Audience claim is allowed.
|
||||||
|
if !verifyOpts.AcceptedAudiences.Contains(t.Claims.Audience) {
|
||||||
|
log.Errorf("token intended for another audience: %q", t.Claims.Audience)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the token is currently usable and not expired.
|
||||||
|
currentUnixTime := time.Now().Unix()
|
||||||
|
if !(t.Claims.NotBefore <= currentUnixTime && currentUnixTime <= t.Claims.Expiration) {
|
||||||
|
log.Errorf("token not to be used before %d or after %d - currently %d", t.Claims.NotBefore, t.Claims.Expiration, currentUnixTime)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the token signature.
|
||||||
|
if len(t.Signature) == 0 {
|
||||||
|
log.Error("token has no signature")
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the token header has a SigningKey field, verify the signature
|
||||||
|
// using that key and its included x509 certificate chain if necessary.
|
||||||
|
// If the Header's SigningKey field is nil, try using the KeyID field.
|
||||||
|
signingKey := t.Header.SigningKey
|
||||||
|
|
||||||
|
if signingKey == nil {
|
||||||
|
// Find the key in the given collection of trusted keys.
|
||||||
|
trustedKey, ok := verifyOpts.TrustedKeys[t.Header.KeyID]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("token signed by untrusted key with ID: %q", t.Header.KeyID)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
signingKey = trustedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// First verify the signature of the token using the key which signed it.
|
||||||
|
if err := signingKey.Verify(strings.NewReader(t.Raw), t.Header.SigningAlg, t.Signature); err != nil {
|
||||||
|
log.Errorf("unable to verify token signature: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, check if the signing key is one of the trusted keys.
|
||||||
|
if _, isTrustedKey := verifyOpts.TrustedKeys[signingKey.KeyID()]; isTrustedKey {
|
||||||
|
// We're done! The token was signed by
|
||||||
|
// a trusted key and has been verified!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we need to check the sigining keys included certificate chain.
|
||||||
|
return t.verifyCertificateChain(signingKey, verifyOpts.Roots)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyCertificateChain attempts to verify the token using the "x5c" field
|
||||||
|
// of the given leafKey which was used to sign it. Returns a nil error if
|
||||||
|
// the key's certificate chain is valid and rooted an one of the given roots.
|
||||||
|
func (t *Token) verifyCertificateChain(leafKey libtrust.PublicKey, roots *x509.CertPool) error {
|
||||||
|
// In this case, the token signature is valid, but the key that signed it
|
||||||
|
// is not in our set of trusted keys. So, we'll need to check if the
|
||||||
|
// token's signing key included an x509 certificate chain that can be
|
||||||
|
// verified up to one of our trusted roots.
|
||||||
|
x5cVal, ok := leafKey.GetExtendedField("x5c").([]interface{})
|
||||||
|
if !ok || x5cVal == nil {
|
||||||
|
log.Error("unable to verify token signature: signed by untrusted key with no valid certificate chain")
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure each item is of the correct type.
|
||||||
|
x5c := make([]string, len(x5cVal))
|
||||||
|
for i, val := range x5cVal {
|
||||||
|
certString, ok := val.(string)
|
||||||
|
if !ok || len(certString) == 0 {
|
||||||
|
log.Error("unable to verify token signature: signed by untrusted key with malformed certificate chain")
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
x5c[i] = certString
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the first element is encoded correctly.
|
||||||
|
leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to decode signing key leaf cert: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// And that it is a valid x509 certificate.
|
||||||
|
leafCert, err := x509.ParseCertificate(leafCertDer)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to parse signing key leaf cert: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the public key in the leaf cert *is* the signing key.
|
||||||
|
leafCryptoKey, ok := leafCert.PublicKey.(crypto.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
log.Error("unable to get signing key leaf cert public key value")
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
leafPubKey, err := libtrust.FromCryptoPublicKey(leafCryptoKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to make libtrust public key from signing key leaf cert: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if leafPubKey.KeyID() != leafKey.KeyID() {
|
||||||
|
log.Error("token signing key ID and leaf certificate public key ID do not match")
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of the x5c array are intermediate certificates.
|
||||||
|
intermediates := x509.NewCertPool()
|
||||||
|
for i := 1; i < len(x5c); i++ {
|
||||||
|
intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to decode signing key intermediate cert: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateCert, err := x509.ParseCertificate(intermediateCertDer)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to parse signing key intermediate cert: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediates.AddCert(intermediateCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyOpts := x509.VerifyOptions{
|
||||||
|
Intermediates: intermediates,
|
||||||
|
Roots: roots,
|
||||||
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this call returns certificate chains which we ignore for now, but
|
||||||
|
// we should check them for revocations if we have the ability later.
|
||||||
|
if _, err = leafCert.Verify(verifyOpts); err != nil {
|
||||||
|
log.Errorf("unable to verify signing key certificate: %s", err)
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// The signing key's x509 chain is valid!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessSet returns a set of actions available for the resource
|
||||||
|
// actions listed in the `access` section of this token.
|
||||||
|
func (t *Token) accessSet() accessSet {
|
||||||
|
if t.Claims == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accessSet := make(accessSet, len(t.Claims.Access))
|
||||||
|
|
||||||
|
for _, resourceActions := range t.Claims.Access {
|
||||||
|
resource := auth.Resource{
|
||||||
|
Type: resourceActions.Type,
|
||||||
|
Name: resourceActions.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
set, exists := accessSet[resource]
|
||||||
|
if !exists {
|
||||||
|
set = newActionSet()
|
||||||
|
accessSet[resource] = set
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range resourceActions.Actions {
|
||||||
|
set.Add(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) compactRaw() string {
|
||||||
|
return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature))
|
||||||
|
}
|
364
auth/token/token_test.go
Normal file
364
auth/token/token_test.go
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/libtrust"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/auth"
|
||||||
|
"github.com/docker/docker-registry/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeRootKeys(numKeys int) ([]libtrust.PrivateKey, error) {
|
||||||
|
keys := make([]libtrust.PrivateKey, 0, numKeys)
|
||||||
|
|
||||||
|
for i := 0; i < numKeys; i++ {
|
||||||
|
key, err := libtrust.GenerateECP256PrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSigningKeyWithChain(rootKey libtrust.PrivateKey, depth int) (libtrust.PrivateKey, error) {
|
||||||
|
if depth == 0 {
|
||||||
|
// Don't need to build a chain.
|
||||||
|
return rootKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
x5c = make([]string, depth)
|
||||||
|
parentKey = rootKey
|
||||||
|
key libtrust.PrivateKey
|
||||||
|
cert *x509.Certificate
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for depth > 0 {
|
||||||
|
if key, err = libtrust.GenerateECP256PrivateKey(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert, err = libtrust.GenerateCACert(parentKey, key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
depth--
|
||||||
|
x5c[depth] = base64.StdEncoding.EncodeToString(cert.Raw)
|
||||||
|
parentKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
key.AddExtendedField("x5c", x5c)
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRootCerts(rootKeys []libtrust.PrivateKey) ([]*x509.Certificate, error) {
|
||||||
|
certs := make([]*x509.Certificate, 0, len(rootKeys))
|
||||||
|
|
||||||
|
for _, key := range rootKeys {
|
||||||
|
cert, err := libtrust.GenerateCACert(key, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certs = append(certs, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTrustedKeyMap(rootKeys []libtrust.PrivateKey) map[string]libtrust.PublicKey {
|
||||||
|
trustedKeys := make(map[string]libtrust.PublicKey, len(rootKeys))
|
||||||
|
|
||||||
|
for _, key := range rootKeys {
|
||||||
|
trustedKeys[key.KeyID()] = key.PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
return trustedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int) (*Token, error) {
|
||||||
|
signingKey, err := makeSigningKeyWithChain(rootKey, depth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to amke signing key with chain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawJWK, err := signingKey.PublicKey().MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to marshal signing key to JSON: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
joseHeader := &Header{
|
||||||
|
Type: "JWT",
|
||||||
|
SigningAlg: "ES256",
|
||||||
|
RawJWK: json.RawMessage(rawJWK),
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
randomBytes := make([]byte, 15)
|
||||||
|
if _, err = rand.Read(randomBytes); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to read random bytes for jwt id: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claimSet := &ClaimSet{
|
||||||
|
Issuer: issuer,
|
||||||
|
Subject: "foo",
|
||||||
|
Audience: audience,
|
||||||
|
Expiration: now.Add(5 * time.Minute).Unix(),
|
||||||
|
NotBefore: now.Unix(),
|
||||||
|
IssuedAt: now.Unix(),
|
||||||
|
JWTID: base64.URLEncoding.EncodeToString(randomBytes),
|
||||||
|
Access: access,
|
||||||
|
}
|
||||||
|
|
||||||
|
var joseHeaderBytes, claimSetBytes []byte
|
||||||
|
|
||||||
|
if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to marshal jose header: %s", err)
|
||||||
|
}
|
||||||
|
if claimSetBytes, err = json.Marshal(claimSet); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to marshal claim set: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedJoseHeader := joseBase64UrlEncode(joseHeaderBytes)
|
||||||
|
encodedClaimSet := joseBase64UrlEncode(claimSetBytes)
|
||||||
|
encodingToSign := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)
|
||||||
|
|
||||||
|
var signatureBytes []byte
|
||||||
|
if signatureBytes, _, err = signingKey.Sign(strings.NewReader(encodingToSign), crypto.SHA256); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to sign jwt payload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := joseBase64UrlEncode(signatureBytes)
|
||||||
|
tokenString := fmt.Sprintf("%s.%s", encodingToSign, signature)
|
||||||
|
|
||||||
|
return NewToken(tokenString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test makes 4 tokens with a varying number of intermediate
|
||||||
|
// certificates ranging from no intermediate chain to a length of 3
|
||||||
|
// intermediates.
|
||||||
|
func TestTokenVerify(t *testing.T) {
|
||||||
|
var (
|
||||||
|
numTokens = 4
|
||||||
|
issuer = "test-issuer"
|
||||||
|
audience = "test-audience"
|
||||||
|
access = []*ResourceActions{
|
||||||
|
{
|
||||||
|
Type: "repository",
|
||||||
|
Name: "foo/bar",
|
||||||
|
Actions: []string{"pull", "push"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
rootKeys, err := makeRootKeys(numTokens)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCerts, err := makeRootCerts(rootKeys)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPool := x509.NewCertPool()
|
||||||
|
for _, rootCert := range rootCerts {
|
||||||
|
rootPool.AddCert(rootCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
trustedKeys := makeTrustedKeyMap(rootKeys)
|
||||||
|
|
||||||
|
tokens := make([]*Token, 0, numTokens)
|
||||||
|
|
||||||
|
for i := 0; i < numTokens; i++ {
|
||||||
|
token, err := makeTestToken(issuer, audience, access, rootKeys[i], i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tokens = append(tokens, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyOps := VerifyOptions{
|
||||||
|
TrustedIssuers: common.NewStringSet(issuer),
|
||||||
|
AcceptedAudiences: common.NewStringSet(audience),
|
||||||
|
Roots: rootPool,
|
||||||
|
TrustedKeys: trustedKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range tokens {
|
||||||
|
if err := token.Verify(verifyOps); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err error) {
|
||||||
|
rootCerts, err := makeRootCerts(rootKeys)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile, err := ioutil.TempFile("", "rootCertBundle")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
for _, cert := range rootCerts {
|
||||||
|
if err = pem.Encode(tempFile, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert.Raw,
|
||||||
|
}); err != nil {
|
||||||
|
os.Remove(tempFile.Name())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempFile.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAccessController tests complete integration of the token auth package.
|
||||||
|
// It starts by mocking the options for a token auth accessController which
|
||||||
|
// it creates. It then tries a few mock requests:
|
||||||
|
// - don't supply a token; should error with challenge
|
||||||
|
// - supply an invalid token; should error with challenge
|
||||||
|
// - supply a token with insufficient access; should error with challenge
|
||||||
|
// - supply a valid token; should not error
|
||||||
|
func TestAccessController(t *testing.T) {
|
||||||
|
// Make 2 keys; only the first is to be a trusted root key.
|
||||||
|
rootKeys, err := makeRootKeys(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCertBundleFilename, err := writeTempRootCerts(rootKeys[:1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(rootCertBundleFilename)
|
||||||
|
|
||||||
|
realm := "https://auth.example.com/token/"
|
||||||
|
issuer := "test-issuer.example.com"
|
||||||
|
service := "test-service.example.com"
|
||||||
|
|
||||||
|
options := map[string]interface{}{
|
||||||
|
"realm": realm,
|
||||||
|
"issuer": issuer,
|
||||||
|
"service": service,
|
||||||
|
"rootCertBundle": rootCertBundleFilename,
|
||||||
|
}
|
||||||
|
|
||||||
|
accessController, err := newAccessController(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Make a mock http.Request with no token.
|
||||||
|
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testAccess := auth.Access{
|
||||||
|
Resource: auth.Resource{
|
||||||
|
Type: "foo",
|
||||||
|
Name: "bar",
|
||||||
|
},
|
||||||
|
Action: "baz",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = accessController.Authorized(req, testAccess)
|
||||||
|
challenge, ok := err.(auth.Challenge)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("accessController did not return a challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
if challenge.Error() != ErrTokenRequired.Error() {
|
||||||
|
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Supply an invalid token.
|
||||||
|
token, err := makeTestToken(
|
||||||
|
issuer, service,
|
||||||
|
[]*ResourceActions{{
|
||||||
|
Type: testAccess.Type,
|
||||||
|
Name: testAccess.Name,
|
||||||
|
Actions: []string{testAccess.Action},
|
||||||
|
}},
|
||||||
|
rootKeys[1], 1, // Everything is valid except the key which signed it.
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
|
||||||
|
|
||||||
|
err = accessController.Authorized(req, testAccess)
|
||||||
|
challenge, ok = err.(auth.Challenge)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("accessController did not return a challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
if challenge.Error() != ErrInvalidToken.Error() {
|
||||||
|
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Supply a token with insufficient access.
|
||||||
|
token, err = makeTestToken(
|
||||||
|
issuer, service,
|
||||||
|
[]*ResourceActions{}, // No access specified.
|
||||||
|
rootKeys[0], 1,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
|
||||||
|
|
||||||
|
err = accessController.Authorized(req, testAccess)
|
||||||
|
challenge, ok = err.(auth.Challenge)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("accessController did not return a challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
if challenge.Error() != ErrInsufficientScope.Error() {
|
||||||
|
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Supply the token we need, or deserve, or whatever.
|
||||||
|
token, err = makeTestToken(
|
||||||
|
issuer, service,
|
||||||
|
[]*ResourceActions{{
|
||||||
|
Type: testAccess.Type,
|
||||||
|
Name: testAccess.Name,
|
||||||
|
Actions: []string{testAccess.Action},
|
||||||
|
}},
|
||||||
|
rootKeys[0], 1, // Everything is valid except the key which signed it.
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
|
||||||
|
|
||||||
|
if err = accessController.Authorized(req, testAccess); err != nil {
|
||||||
|
t.Fatalf("accessController returned unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
49
auth/token/util.go
Normal file
49
auth/token/util.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// joseBase64UrlEncode encodes the given data using the standard base64 url
|
||||||
|
// encoding format but with all trailing '=' characters ommitted in accordance
|
||||||
|
// with the jose specification.
|
||||||
|
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
|
||||||
|
func joseBase64UrlEncode(b []byte) string {
|
||||||
|
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
||||||
|
}
|
||||||
|
|
||||||
|
// joseBase64UrlDecode decodes the given string using the standard base64 url
|
||||||
|
// decoder but first adds the appropriate number of trailing '=' characters in
|
||||||
|
// accordance with the jose specification.
|
||||||
|
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
|
||||||
|
func joseBase64UrlDecode(s string) ([]byte, error) {
|
||||||
|
switch len(s) % 4 {
|
||||||
|
case 0:
|
||||||
|
case 2:
|
||||||
|
s += "=="
|
||||||
|
case 3:
|
||||||
|
s += "="
|
||||||
|
default:
|
||||||
|
return nil, errors.New("illegal base64url string")
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.DecodeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionSet is a special type of stringSet.
|
||||||
|
type actionSet struct {
|
||||||
|
common.StringSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionSet(actions ...string) actionSet {
|
||||||
|
return actionSet{common.NewStringSet(actions...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains calls StringSet.Contains() for
|
||||||
|
// either "*" or the given action string.
|
||||||
|
func (s actionSet) Contains(action string) bool {
|
||||||
|
return s.StringSet.Contains("*") || s.StringSet.Contains(action)
|
||||||
|
}
|
35
common/stringset.go
Normal file
35
common/stringset.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
// StringSet is a useful type for looking up strings.
|
||||||
|
type StringSet map[string]struct{}
|
||||||
|
|
||||||
|
// NewStringSet creates a new StringSet with the given strings.
|
||||||
|
func NewStringSet(keys ...string) StringSet {
|
||||||
|
ss := make(StringSet, len(keys))
|
||||||
|
ss.Add(keys...)
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inserts the given keys into this StringSet.
|
||||||
|
func (ss StringSet) Add(keys ...string) {
|
||||||
|
for _, key := range keys {
|
||||||
|
ss[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns whether the given key is in this StringSet.
|
||||||
|
func (ss StringSet) Contains(key string) bool {
|
||||||
|
_, ok := ss[key]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns a slice of all keys in this StringSet.
|
||||||
|
func (ss StringSet) Keys() []string {
|
||||||
|
keys := make([]string, 0, len(ss))
|
||||||
|
|
||||||
|
for key := range ss {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
Loading…
Reference in a new issue