Merge pull request #58544 from ericchiang/oidc-v2
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. oidc authentication: switch to v2 of coreos/go-oidc Switch to v2 of [coreos/go-oidc](https://github.com/coreos/go-oidc), which uses square/go-jose to verify tokens and supports more signing algorithms. Most of this PR removes dependencies used by the older version of github.com/coreos/go-oidc, and updates vendor files. This PR has been tested against tokens issued by Okta, Google, and CoreOS's dex. Closes https://github.com/kubernetes/kubernetes/issues/57806 ```release-note kube-apiserver: the OpenID Connect authenticator can now verify ID Tokens signed with JOSE algorithms other than RS256 through the --oidc-signing-algs flag. kube-apiserver: the OpenID Connect authenticator no longer accepts tokens from the Google v3 token APIs, users must switch to the "https://www.googleapis.com/oauth2/v4/token" endpoint. ``` cc @rithujohn191 @liggitt cc @kubernetes/sig-auth-pr-reviews Kubernetes-commit: cdbc4fbe20c94694bc25910d54a7de52a98b6650
This commit is contained in:
		
						commit
						4df1acfefd
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -9,12 +9,12 @@ load( | |||
| go_test( | ||||
|     name = "go_default_test", | ||||
|     srcs = ["oidc_test.go"], | ||||
|     data = glob(["testdata/**"]), | ||||
|     embed = [":go_default_library"], | ||||
|     deps = [ | ||||
|         "//vendor/github.com/coreos/go-oidc/jose:go_default_library", | ||||
|         "//vendor/github.com/coreos/go-oidc/oidc:go_default_library", | ||||
|         "//vendor/github.com/coreos/go-oidc:go_default_library", | ||||
|         "//vendor/gopkg.in/square/go-jose.v2:go_default_library", | ||||
|         "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", | ||||
|         "//vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/testing:go_default_library", | ||||
|     ], | ||||
| ) | ||||
| 
 | ||||
|  | @ -23,11 +23,10 @@ go_library( | |||
|     srcs = ["oidc.go"], | ||||
|     importpath = "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc", | ||||
|     deps = [ | ||||
|         "//vendor/github.com/coreos/go-oidc/jose:go_default_library", | ||||
|         "//vendor/github.com/coreos/go-oidc/oidc:go_default_library", | ||||
|         "//vendor/github.com/coreos/go-oidc:go_default_library", | ||||
|         "//vendor/github.com/golang/glog:go_default_library", | ||||
|         "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", | ||||
|         "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", | ||||
|         "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", | ||||
|         "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", | ||||
|         "//vendor/k8s.io/client-go/util/cert:go_default_library", | ||||
|     ], | ||||
|  | @ -42,9 +41,6 @@ filegroup( | |||
| 
 | ||||
| filegroup( | ||||
|     name = "all-srcs", | ||||
|     srcs = [ | ||||
|         ":package-srcs", | ||||
|         "//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/testing:all-srcs", | ||||
|     ], | ||||
|     srcs = [":package-srcs"], | ||||
|     tags = ["automanaged"], | ||||
| ) | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ limitations under the License. | |||
| /* | ||||
| oidc implements the authenticator.Token interface using the OpenID Connect protocol. | ||||
| 
 | ||||
| 	config := oidc.OIDCOptions{ | ||||
| 	config := oidc.Options{ | ||||
| 		IssuerURL:     "https://accounts.google.com", | ||||
| 		ClientID:      os.Getenv("GOOGLE_CLIENT_ID"), | ||||
| 		UsernameClaim: "email", | ||||
|  | @ -27,25 +27,28 @@ oidc implements the authenticator.Token interface using the OpenID Connect proto | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"sync" | ||||
| 	"strings" | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| 	"github.com/coreos/go-oidc/oidc" | ||||
| 	oidc "github.com/coreos/go-oidc" | ||||
| 	"github.com/golang/glog" | ||||
| 	"k8s.io/apimachinery/pkg/util/net" | ||||
| 	"k8s.io/apimachinery/pkg/util/runtime" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"k8s.io/apiserver/pkg/authentication/user" | ||||
| 	certutil "k8s.io/client-go/util/cert" | ||||
| ) | ||||
| 
 | ||||
| type OIDCOptions struct { | ||||
| type Options struct { | ||||
| 	// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss"
 | ||||
| 	// field of all tokens produced by the provider and is used for configuration
 | ||||
| 	// discovery.
 | ||||
|  | @ -83,30 +86,85 @@ type OIDCOptions struct { | |||
| 	// GroupsPrefix, if specified, causes claims mapping to group names to be prefixed with the
 | ||||
| 	// value. A value "oidc:" would result in groups like "oidc:engineering" and "oidc:marketing".
 | ||||
| 	GroupsPrefix string | ||||
| 
 | ||||
| 	// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that
 | ||||
| 	// can be used by the provider to sign tokens.
 | ||||
| 	//
 | ||||
| 	// https://tools.ietf.org/html/rfc7518#section-3.1
 | ||||
| 	//
 | ||||
| 	// This value defaults to RS256, the value recommended by the OpenID Connect
 | ||||
| 	// spec:
 | ||||
| 	//
 | ||||
| 	// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
 | ||||
| 	SupportedSigningAlgs []string | ||||
| 
 | ||||
| 	// now is used for testing. It defaults to time.Now.
 | ||||
| 	now func() time.Time | ||||
| } | ||||
| 
 | ||||
| type OIDCAuthenticator struct { | ||||
| type Authenticator struct { | ||||
| 	issuerURL string | ||||
| 
 | ||||
| 	trustedClientID string | ||||
| 
 | ||||
| 	usernameClaim  string | ||||
| 	usernamePrefix string | ||||
| 	groupsClaim    string | ||||
| 	groupsPrefix   string | ||||
| 
 | ||||
| 	httpClient *http.Client | ||||
| 	// Contains an *oidc.IDTokenVerifier. Do not access directly use the
 | ||||
| 	// idTokenVerifier method.
 | ||||
| 	verifier atomic.Value | ||||
| 
 | ||||
| 	// Contains an *oidc.Client. Do not access directly. Use client() method.
 | ||||
| 	oidcClient atomic.Value | ||||
| 
 | ||||
| 	// Guards the close method and is used to lock during initialization and closing.
 | ||||
| 	mu    sync.Mutex | ||||
| 	close func() // May be nil
 | ||||
| 	cancel context.CancelFunc | ||||
| } | ||||
| 
 | ||||
| // New creates a token authenticator which validates OpenID Connect ID Tokens.
 | ||||
| func New(opts OIDCOptions) (*OIDCAuthenticator, error) { | ||||
| func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) { | ||||
| 	a.verifier.Store(v) | ||||
| } | ||||
| 
 | ||||
| func (a *Authenticator) idTokenVerifier() (*oidc.IDTokenVerifier, bool) { | ||||
| 	if v := a.verifier.Load(); v != nil { | ||||
| 		return v.(*oidc.IDTokenVerifier), true | ||||
| 	} | ||||
| 	return nil, false | ||||
| } | ||||
| 
 | ||||
| func (a *Authenticator) Close() { | ||||
| 	a.cancel() | ||||
| } | ||||
| 
 | ||||
| func New(opts Options) (*Authenticator, error) { | ||||
| 	return newAuthenticator(opts, func(ctx context.Context, a *Authenticator, config *oidc.Config) { | ||||
| 		// Asynchronously attempt to initialize the authenticator. This enables
 | ||||
| 		// self-hosted providers, providers that run on top of Kubernetes itself.
 | ||||
| 		go wait.PollUntil(time.Second*10, func() (done bool, err error) { | ||||
| 			provider, err := oidc.NewProvider(ctx, a.issuerURL) | ||||
| 			if err != nil { | ||||
| 				glog.Errorf("oidc authenticator: initializing plugin: %v", err) | ||||
| 				return false, nil | ||||
| 			} | ||||
| 
 | ||||
| 			verifier := provider.Verifier(config) | ||||
| 			a.setVerifier(verifier) | ||||
| 			return true, nil | ||||
| 		}, ctx.Done()) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // whitelist of signing algorithms to ensure users don't mistakenly pass something
 | ||||
| // goofy.
 | ||||
| var allowedSigningAlgs = map[string]bool{ | ||||
| 	oidc.RS256: true, | ||||
| 	oidc.RS384: true, | ||||
| 	oidc.RS512: true, | ||||
| 	oidc.ES256: true, | ||||
| 	oidc.ES384: true, | ||||
| 	oidc.ES512: true, | ||||
| 	oidc.PS256: true, | ||||
| 	oidc.PS384: true, | ||||
| 	oidc.PS512: true, | ||||
| } | ||||
| 
 | ||||
| func newAuthenticator(opts Options, initVerifier func(ctx context.Context, a *Authenticator, config *oidc.Config)) (*Authenticator, error) { | ||||
| 	url, err := url.Parse(opts.IssuerURL) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  | @ -120,6 +178,18 @@ func New(opts OIDCOptions) (*OIDCAuthenticator, error) { | |||
| 		return nil, errors.New("no username claim provided") | ||||
| 	} | ||||
| 
 | ||||
| 	supportedSigningAlgs := opts.SupportedSigningAlgs | ||||
| 	if len(supportedSigningAlgs) == 0 { | ||||
| 		// RS256 is the default recommended by OpenID Connect and an 'alg' value
 | ||||
| 		// providers are required to implement.
 | ||||
| 		supportedSigningAlgs = []string{oidc.RS256} | ||||
| 	} | ||||
| 	for _, alg := range supportedSigningAlgs { | ||||
| 		if !allowedSigningAlgs[alg] { | ||||
| 			return nil, fmt.Errorf("oidc: unsupported signing alg: %q", alg) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var roots *x509.CertPool | ||||
| 	if opts.CAFile != "" { | ||||
| 		roots, err = certutil.NewPool(opts.CAFile) | ||||
|  | @ -137,137 +207,91 @@ func New(opts OIDCOptions) (*OIDCAuthenticator, error) { | |||
| 		TLSClientConfig: &tls.Config{RootCAs: roots}, | ||||
| 	}) | ||||
| 
 | ||||
| 	authenticator := &OIDCAuthenticator{ | ||||
| 		issuerURL:       opts.IssuerURL, | ||||
| 		trustedClientID: opts.ClientID, | ||||
| 		usernameClaim:   opts.UsernameClaim, | ||||
| 		usernamePrefix:  opts.UsernamePrefix, | ||||
| 		groupsClaim:     opts.GroupsClaim, | ||||
| 		groupsPrefix:    opts.GroupsPrefix, | ||||
| 		httpClient:      &http.Client{Transport: tr}, | ||||
| 	client := &http.Client{Transport: tr, Timeout: 30 * time.Second} | ||||
| 
 | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	ctx = oidc.ClientContext(ctx, client) | ||||
| 
 | ||||
| 	authenticator := &Authenticator{ | ||||
| 		issuerURL:      opts.IssuerURL, | ||||
| 		usernameClaim:  opts.UsernameClaim, | ||||
| 		usernamePrefix: opts.UsernamePrefix, | ||||
| 		groupsClaim:    opts.GroupsClaim, | ||||
| 		groupsPrefix:   opts.GroupsPrefix, | ||||
| 		cancel:         cancel, | ||||
| 	} | ||||
| 
 | ||||
| 	// Attempt to initialize the authenticator asynchronously.
 | ||||
| 	//
 | ||||
| 	// Ignore errors instead of returning it since the OpenID Connect provider might not be
 | ||||
| 	// available yet, for instance if it's running on the cluster and needs the API server
 | ||||
| 	// to come up first. Errors will be logged within the client() method.
 | ||||
| 	go func() { | ||||
| 		defer runtime.HandleCrash() | ||||
| 		authenticator.client() | ||||
| 	}() | ||||
| 	now := opts.now | ||||
| 	if now == nil { | ||||
| 		now = time.Now | ||||
| 	} | ||||
| 
 | ||||
| 	verifierConfig := &oidc.Config{ | ||||
| 		ClientID:             opts.ClientID, | ||||
| 		SupportedSigningAlgs: supportedSigningAlgs, | ||||
| 		Now:                  now, | ||||
| 	} | ||||
| 
 | ||||
| 	initVerifier(ctx, authenticator, verifierConfig) | ||||
| 	return authenticator, nil | ||||
| } | ||||
| 
 | ||||
| // Close stops all goroutines used by the authenticator.
 | ||||
| func (a *OIDCAuthenticator) Close() { | ||||
| 	a.mu.Lock() | ||||
| 	defer a.mu.Unlock() | ||||
| 
 | ||||
| 	if a.close != nil { | ||||
| 		a.close() | ||||
| func hasCorrectIssuer(iss, tokenData string) bool { | ||||
| 	parts := strings.Split(tokenData, ".") | ||||
| 	if len(parts) != 3 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return | ||||
| 	payload, err := base64.RawURLEncoding.DecodeString(parts[1]) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	claims := struct { | ||||
| 		// WARNING: this JWT is not verified. Do not trust these claims.
 | ||||
| 		Issuer string `json:"iss"` | ||||
| 	}{} | ||||
| 	if err := json.Unmarshal(payload, &claims); err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	if claims.Issuer != iss { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func (a *OIDCAuthenticator) client() (*oidc.Client, error) { | ||||
| 	// Fast check to see if client has already been initialized.
 | ||||
| 	if client := a.oidcClient.Load(); client != nil { | ||||
| 		return client.(*oidc.Client), nil | ||||
| func (a *Authenticator) AuthenticateToken(token string) (user.Info, bool, error) { | ||||
| 	if !hasCorrectIssuer(a.issuerURL, token) { | ||||
| 		return nil, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// Acquire lock, then recheck initialization.
 | ||||
| 	a.mu.Lock() | ||||
| 	defer a.mu.Unlock() | ||||
| 	if client := a.oidcClient.Load(); client != nil { | ||||
| 		return client.(*oidc.Client), nil | ||||
| 	} | ||||
| 
 | ||||
| 	// Try to initialize client.
 | ||||
| 	providerConfig, err := oidc.FetchProviderConfig(a.httpClient, a.issuerURL) | ||||
| 	if err != nil { | ||||
| 		glog.Errorf("oidc authenticator: failed to fetch provider discovery data: %v", err) | ||||
| 		return nil, fmt.Errorf("fetch provider config: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	clientConfig := oidc.ClientConfig{ | ||||
| 		HTTPClient:     a.httpClient, | ||||
| 		Credentials:    oidc.ClientCredentials{ID: a.trustedClientID}, | ||||
| 		ProviderConfig: providerConfig, | ||||
| 	} | ||||
| 
 | ||||
| 	client, err := oidc.NewClient(clientConfig) | ||||
| 	if err != nil { | ||||
| 		glog.Errorf("oidc authenticator: failed to create client: %v", err) | ||||
| 		return nil, fmt.Errorf("create client: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// SyncProviderConfig will start a goroutine to periodically synchronize the provider config.
 | ||||
| 	// The synchronization interval is set by the expiration length of the config, and has a minimum
 | ||||
| 	// and maximum threshold.
 | ||||
| 	stop := client.SyncProviderConfig(a.issuerURL) | ||||
| 	a.oidcClient.Store(client) | ||||
| 	a.close = func() { | ||||
| 		// This assumes the stop is an unbuffered channel.
 | ||||
| 		// So instead of closing the channel, we send am empty struct here.
 | ||||
| 		// This guarantees that when this function returns, there is no flying requests,
 | ||||
| 		// because a send to an unbuffered channel happens after the receive from the channel.
 | ||||
| 		stop <- struct{}{} | ||||
| 	} | ||||
| 	return client, nil | ||||
| } | ||||
| 
 | ||||
| // AuthenticateToken decodes and verifies an ID Token using the OIDC client, if the verification succeeds,
 | ||||
| // then it will extract the user info from the JWT claims.
 | ||||
| func (a *OIDCAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) { | ||||
| 	jwt, err := jose.ParseJWT(value) | ||||
| 	if err != nil { | ||||
| 		return nil, false, err | ||||
| 	} | ||||
| 
 | ||||
| 	client, err := a.client() | ||||
| 	if err != nil { | ||||
| 		return nil, false, err | ||||
| 	} | ||||
| 	if err := client.VerifyJWT(jwt); err != nil { | ||||
| 		return nil, false, err | ||||
| 	} | ||||
| 	claims, err := jwt.Claims() | ||||
| 	if err != nil { | ||||
| 		return nil, false, err | ||||
| 	} | ||||
| 	return a.parseTokenClaims(claims) | ||||
| } | ||||
| 
 | ||||
| // parseTokenClaims maps a set of claims to a user. It performs basic validation such as
 | ||||
| // ensuring the email is verified.
 | ||||
| func (a *OIDCAuthenticator) parseTokenClaims(claims jose.Claims) (user.Info, bool, error) { | ||||
| 	username, ok, err := claims.StringClaim(a.usernameClaim) | ||||
| 	if err != nil { | ||||
| 		return nil, false, err | ||||
| 	} | ||||
| 	ctx := context.Background() | ||||
| 	verifier, ok := a.idTokenVerifier() | ||||
| 	if !ok { | ||||
| 		return nil, false, fmt.Errorf("cannot find %q in JWT claims", a.usernameClaim) | ||||
| 		return nil, false, fmt.Errorf("oidc: authenticator not initialized") | ||||
| 	} | ||||
| 
 | ||||
| 	idToken, err := verifier.Verify(ctx, token) | ||||
| 	if err != nil { | ||||
| 		return nil, false, fmt.Errorf("oidc: verify token: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	var c claims | ||||
| 	if err := idToken.Claims(&c); err != nil { | ||||
| 		return nil, false, fmt.Errorf("oidc: parse claims: %v", err) | ||||
| 	} | ||||
| 	var username string | ||||
| 	if err := c.unmarshalClaim(a.usernameClaim, &username); err != nil { | ||||
| 		return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.usernameClaim, err) | ||||
| 	} | ||||
| 
 | ||||
| 	if a.usernameClaim == "email" { | ||||
| 		verified, ok := claims["email_verified"] | ||||
| 		if !ok { | ||||
| 			return nil, false, errors.New("'email_verified' claim not present") | ||||
| 		// Check the email_verified claim to ensure the email is valid.
 | ||||
| 		// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
 | ||||
| 		var emailVerified bool | ||||
| 		if err := c.unmarshalClaim("email_verified", &emailVerified); err != nil { | ||||
| 			return nil, false, fmt.Errorf("oidc: parse 'email_verified' claim: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 		emailVerified, ok := verified.(bool) | ||||
| 		if !ok { | ||||
| 			// OpenID Connect spec defines 'email_verified' as a boolean. For now, be a pain and error if
 | ||||
| 			// it's a different type. If there are enough misbehaving providers we can relax this latter.
 | ||||
| 			//
 | ||||
| 			// See: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
 | ||||
| 			return nil, false, fmt.Errorf("malformed claim 'email_verified', expected boolean got %T", verified) | ||||
| 		} | ||||
| 
 | ||||
| 		if !emailVerified { | ||||
| 			return nil, false, errors.New("email not verified") | ||||
| 			return nil, false, fmt.Errorf("oidc: email not verified") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -275,21 +299,18 @@ func (a *OIDCAuthenticator) parseTokenClaims(claims jose.Claims) (user.Info, boo | |||
| 		username = a.usernamePrefix + username | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(yifan): Add UID, also populate the issuer to upper layer.
 | ||||
| 	info := &user.DefaultInfo{Name: username} | ||||
| 
 | ||||
| 	if a.groupsClaim != "" { | ||||
| 		groups, found, err := claims.StringsClaim(a.groupsClaim) | ||||
| 		if err != nil { | ||||
| 			// Groups type is present but is not an array of strings, try to decode as a string.
 | ||||
| 			group, _, err := claims.StringClaim(a.groupsClaim) | ||||
| 			if err != nil { | ||||
| 				// Custom claim is present, but isn't an array of strings or a string.
 | ||||
| 				return nil, false, fmt.Errorf("custom group claim contains invalid type: %T", claims[a.groupsClaim]) | ||||
| 		if _, ok := c[a.groupsClaim]; ok { | ||||
| 			// Some admins want to use string claims like "role" as the group value.
 | ||||
| 			// Allow the group claim to be a single string instead of an array.
 | ||||
| 			//
 | ||||
| 			// See: https://github.com/kubernetes/kubernetes/issues/33290
 | ||||
| 			var groups stringOrArray | ||||
| 			if err := c.unmarshalClaim(a.groupsClaim, &groups); err != nil { | ||||
| 				return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.groupsClaim, err) | ||||
| 			} | ||||
| 			info.Groups = []string{group} | ||||
| 		} else if found { | ||||
| 			info.Groups = groups | ||||
| 			info.Groups = []string(groups) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -298,6 +319,31 @@ func (a *OIDCAuthenticator) parseTokenClaims(claims jose.Claims) (user.Info, boo | |||
| 			info.Groups[i] = a.groupsPrefix + group | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return info, true, nil | ||||
| } | ||||
| 
 | ||||
| type stringOrArray []string | ||||
| 
 | ||||
| func (s *stringOrArray) UnmarshalJSON(b []byte) error { | ||||
| 	var a []string | ||||
| 	if err := json.Unmarshal(b, &a); err == nil { | ||||
| 		*s = a | ||||
| 		return nil | ||||
| 	} | ||||
| 	var str string | ||||
| 	if err := json.Unmarshal(b, &str); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*s = []string{str} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type claims map[string]json.RawMessage | ||||
| 
 | ||||
| func (c claims) unmarshalClaim(name string, v interface{}) error { | ||||
| 	val, ok := c[name] | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("claim not present") | ||||
| 	} | ||||
| 	return json.Unmarshal([]byte(val), v) | ||||
| } | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,7 @@ | |||
| -----BEGIN EC PRIVATE KEY----- | ||||
| MIHcAgEBBEIAvxtZy9aI/lEt6LVLIhVrWLSwmlMFThU/nZUPr88nA5yKzJMyKbW/ | ||||
| QA+umam9YtO78WwzriTGC9qwW6+t+liXrcCgBwYFK4EEACOhgYkDgYYABAGzIO9n | ||||
| tdTx6oVg1O59ljYP4FHY9RNUy+wHeXFnB6fo9asGg9jwLMg/iX0F+whFkllQjNLf | ||||
| kKp/9ATWQHrzSbzuqwB9UU5zfQ3ulhMwEBpxbM6aSi1HyYtc5pQn7KB6h1VXiuQK | ||||
| CIj4kVYHClZuKz0om/XAJL4vWVDwJqDBN6m9Yi9ZLQ== | ||||
| -----END EC PRIVATE KEY----- | ||||
|  | @ -0,0 +1,7 @@ | |||
| -----BEGIN EC PRIVATE KEY----- | ||||
| MIHcAgEBBEIBtxiwrrDmi1U+NxClUKIr2cvmL6PPLxjAULjPuORt0AWbqKakphSJ | ||||
| 43VmIbPBBCuLnN2PuVS9N8jLDlR1KUnnFSGgBwYFK4EEACOhgYkDgYYABAEgshGY | ||||
| Oflwnz2SQOWIkvSPmijMhS4nWmLYedR2H/Dg9c9nuiyQqL3XpqkPnQQwqOgcXjMT | ||||
| hTec2tiLcRS3Gj02yQEpe/6Do6if4K4cQ9KsNtVHsn0bibsqLtRuvI7xUu9JJAs7 | ||||
| vSLNUtmxVzFo4s4spnIjLT71uz1Vag/NrKwot7cz4g== | ||||
| -----END EC PRIVATE KEY----- | ||||
|  | @ -0,0 +1,7 @@ | |||
| -----BEGIN EC PRIVATE KEY----- | ||||
| MIHcAgEBBEIAQTHP4V93rz1w1D+jF1Jvx5QHzkQQIYxbN1LPuvQjEoplQrjZ4Qiu | ||||
| h9mKK6DBCaDlfSos+wnTOlZH1z1tpa9soPOgBwYFK4EEACOhgYkDgYYABAFn+QOY | ||||
| a937Lp+WO1S+zJU9ITnzdvjqQtD/TjtJPQsllV8rD0QNXZb/pLFQFZtDEehiZKEu | ||||
| WA0REGNs+rVMO63YZAAyDMwZTz87ulH23OR6EaoyDp9qEPx7kpxgaJqeIztla2t8 | ||||
| SLVpv/FPR92E/OmguT6sFI5mP0AhV8UVlLYuHaovnw== | ||||
| -----END EC PRIVATE KEY----- | ||||
|  | @ -0,0 +1,27 @@ | |||
| #!/bin/bash -e | ||||
| 
 | ||||
| # Copyright 2018 The Kubernetes Authors. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| 
 | ||||
| rm *.pem | ||||
| 
 | ||||
| for N in `seq 1 3`; do | ||||
|     ssh-keygen -t rsa -b 2048 -f rsa_$N.pem -N '' | ||||
| done | ||||
| 
 | ||||
| for N in `seq 1 3`; do | ||||
|     ssh-keygen -t ecdsa -b 521 -f ecdsa_$N.pem -N '' | ||||
| done | ||||
| 
 | ||||
| rm *.pub | ||||
|  | @ -0,0 +1,27 @@ | |||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| MIIEpAIBAAKCAQEA0pXWMYjWRjBEds/fKj/u9r2E6SIDx0J+TAg+eyVeR20Ky9jZ | ||||
| mIXW5zSxE/EKpNQpiBWm1e6G9kmhMuqjr7g455S7E+3rD3OVkdTT6SU5AKBNSFoR | ||||
| XUd+G/YJEtRzrpEYNtEJHkxUxWuyfCHblHSt+wsrE6t0DccCqC87lKQiGb/QfC8u | ||||
| P6ZS99SCjKBEFp1fZvyNkYwStFc2OH5fBGPXXb6SNsquvDeKX9NeWjXkmxDkbOg2 | ||||
| kSkel4s/zw5KwcW3JzERfEcLStrDQ8fRbJ1C3uC088sUk4q4APQmKI/8FTvJe431 | ||||
| Vne9sOSptphiqCjlR+Knja58rc/vt4TkSPZf2wIDAQABAoIBAQDO3UgjMtOi8Wlf | ||||
| +YW1IEbjdXrp9XMWu9gLYpHWMPgzXAeeBfCDJv7b8uP8ve2By7TcrMBOKVnE+MF0 | ||||
| nhCb3nFv9KftxOsDK70DG7nrrpgXaGFisK+cHU3hs8hoCfF1y6yotKGrdLpVkR0t | ||||
| Wak1ZYU/NlJjqSqBGj0e7/8sXivtc7oME8tBBRBCEa8OqPqaelCInfFF1rX5vmxX | ||||
| pQjPpZoA+vroSJy8SYE0N5oqtGwOPT+9rVuDOL10eaMbGUcssZl8ofwuvzOYPMW4 | ||||
| KFSVtvdtKnACq94Qy6XQbK5hZbZXSpzxANKq8SFyG2N1wOlpu/ktdXqkyDs08AZY | ||||
| c/KkpXspAoGBAPdC73GOZn/hxzkwZ2Dl+S9rgrLT3VbeuhMp6GXSdiT+f9babMuw | ||||
| HlYw6uULmvL1gD/0GmyWrHopPFJxodBG5SlwYS5wl49slcxeKCjK26vbNfK2eCbu | ||||
| 9uMtED4dN/5NlaXF4hqy/FmSyaFhQT+5hvx8n/zvLsgpuSQ+SCiDAHMfAoGBANoH | ||||
| FCZeCWzzUFhObYG9wxGJ9FBPQa0htafIYEgTwezlKPsrfXfCTnVg1lLkr6Z4IwYQ | ||||
| 9VufJZNAc5V0X9H/ceyKJYxhQ+E01NEVzVpoK8fOC4yCYSYtbJnqkOUQzZJzkjFT | ||||
| mNcIa8o4UrBOWzMhMQa0AOZH4VrbtZDCZhid+hfFAoGAAbKh9kOmDIa+WXQtoYqy | ||||
| tVKlqRivUmNhH7SP9fMGAKcGtbD2QkfJTYo0crIrtDNfWBETBV/be1NBKMfC9q0l | ||||
| 8azl3e3D/KYgOTEEUZNjAsEUk8AQ/yNw6opqrCKDOenKd0LulIRaGztYyxTh39Ak | ||||
| TyOD7bauuY0fylHrKOwNWr0CgYEAsVZ0o0h1rjKyNUGFfLQWyFtHZ1Mv/lye3tvy | ||||
| xG2dnMxAaxvSr+hR3NNpQH9WB7dL9ZExoNZvv7f6y6OelLaLuXQcWnR6u+E3AOIU | ||||
| 5+Y3RgtoBV+/GUh1PzQ1qrviGa77SDfQ54an9hGd4F27fHkQ4XzkBmqM+FQg+J/G | ||||
| X1uPomkCgYBo4ZBEA20Wvf1k2iWOVdfsxZNeOLxwcN5x89dAvm0v6RHg2NMy+XKw | ||||
| Rj+YRuudFdxfg39J/V/Md9qsvjW+4FthD8GhgPs22dksV+7j6ApWkYTmIKG4rmh3 | ||||
| RhHOr6uLg9BeShnlvMMaMJKf2eA7SaVtmuS6uBGgEUNaa3qEBq0R+Q== | ||||
| -----END RSA PRIVATE KEY----- | ||||
|  | @ -0,0 +1,27 @@ | |||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| MIIEowIBAAKCAQEAxmLv1fqjacxZu5jyfQzytwjdt9/BBDzPM6G+R92YvsB7o2FH | ||||
| ISiySbT8pyj7wGUgDvBdc9+GT3thf2pKGl/THaYJCLbh90GXVMTTak8Najqp2Qod | ||||
| kxIpjSOBgukmd0LUtUVGKJg7YTEqsxAUmn9mfFiUZ6ixHr0U2NAXu7TF4dw7C9bt | ||||
| ORi9DKDfUhEiV3Vcyrc5d/9VdmWG6fs/kq0B/6AboiSIIGAKSugNi4BgV7rNZYwZ | ||||
| 37UT72CW28TQQuuAtmTgXWMkfeWZiQKD1P8Nl1byORpgivp2A+2pMNlcLuogVhGY | ||||
| TIZstbeMqoPyLW57GitWDg1tbakwJdCR23gCKQIDAQABAoIBACZmDf//1FNtD01F | ||||
| TGIx+GS/HZMyhvyX/I8E1ny4gpEhVo0IDil35BJqKqD8SMYzjKH3mk8MS8Xknrl3 | ||||
| zEIQnB9X/NWn+FLQakcpFba0+GbAVhHBaHoIAOzlm3LIR/67e8peTzcaSBwG1Tn1 | ||||
| eddxo1ecGZV6zFWjyX4xwPY/BjIyA/b1LewqIK6W/I4u3RwtEzqANV9ddVbFH53e | ||||
| Y+i2X1HVuLm0LETsX4jB/G/ZDP6Y101gOwPFddm+h1ixZ2jrAkyTbvYL5ukIsU8Z | ||||
| okIEZsd6nv08YN+LOXOPh0CxvgHI347RDzgfbDmHGqq8gh20+wLP/MV+dOiBBAJT | ||||
| RfnoFcUCgYEA8SpMW64CNhRkH3Nv5A5ffSOa7NqiN7OdNEswgcgkAbjR5YsTATPg | ||||
| p9iWqGcEodX1FWjnL2NLMdMvYxh4MwMCACIa8FQ2/RDEwViaVjxcOK64MIvyvnNq | ||||
| NObx8pMClUBXWF/llxxTR+/CJWRdCABBm56lQPuuX/qEi/xqybHPcAcCgYEA0pb9 | ||||
| FGmGhDXl3bG3zNNIim+FuqG0xQoIjVPwdvkMI30h/ii6qs3jxwHlw6KBf8AI9wI+ | ||||
| bWbzhwcYVkS6It0Yj4q/mqOVHi89frrAQygsJkMQkdl8WiWwPeiiIdsHYTUcBv5+ | ||||
| i6YLs8ycnzMeFAxg8kuxrq6mm3yW6u5CuInsEE8CgYAWXqUMj/x2hbevzyZe0hJ7 | ||||
| ahURyUnovsljM2JBd44XdsxJbXgK0YQSLZ3z6vJcDJuaK8vd8mjkK0GnAHsNyEak | ||||
| OoWjKzyahrapdI2EWD75pwNAxYpzrgL4+z8QECDaNUik0uhZ9u+mqY+ppkCW4Gc1 | ||||
| hyau+2l2T6eB0J0bLloeewKBgQC+fZn8JuBpI6AEg8ew3cYWg37CLZgpTEQkIzO3 | ||||
| StyyFXT0RL9l1cwerha6ensNphX16e+yYpgTIlXfY1fERZ776RQcu7Adl7nWsvNL | ||||
| TEFzcuLAK60SlljwB0jxuwDX64SoxviNNewL/iAG2eRxWikvw0y8qHtI1tBlPpTX | ||||
| /NqufQKBgD1jAPRgu+IN9IDXoqnMiXbSfrVsJ1ndyPCRBq4dfvawcbEDTZfkiid0 | ||||
| aNrdRoOETfLCynAcG6o6uS3+2sumzXLrMNr8MDF8NEa2Rh8nN3IjZqESV4CNgaV6 | ||||
| JhAlWFp+AvYv6N1iHK52nNAFiX/cfaMpWTUKqk2Z4VZCr5qhLUVs | ||||
| -----END RSA PRIVATE KEY----- | ||||
|  | @ -0,0 +1,27 @@ | |||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| MIIEpgIBAAKCAQEA1uO3LV0wHadHjQb8U8KGUpzlZTyBHlinL7lF5yKk2hXOyssT | ||||
| Z0UVF6ofPq/L+ZN5VkTb1FJWMEBiX1BgXlboDdYKAYTl1QaeEQrfrAM4gp2FdWS4 | ||||
| fMUuagFdVXs8T2J4GGPEE83ybwz/YEM3p83Qojifvx/IjVtuHCMkUpcj92scjsCY | ||||
| EeSKRjJicVBmJ1RK0ShEorHhnYmokKYsPl41mV5VdmWZOtmPK4jV05cBfg7eC7yI | ||||
| cwwYhowLkYvd9d9H74otK5tD7KTxFG6JJ0N5zwf6XBRcXBWKstfOOZ5Qgo9z0Tkm | ||||
| n3Klp9vwun25aaA1MlSSByByiCb7qvS8jhqMkQIDAQABAoIBAQCjN58AU9GiFFai | ||||
| ZXXuYMgJo6YRON2RoSCLfRv9LTEtfHbvTUPVooEc0lidEoXJcRwuTGr0X/2a9KxD | ||||
| XRd1UGk9aR98e+bd4QLaSvoM+v1HKEIgInqGOnbAiXzM2qe6XD5/t/dMW5cShjrK | ||||
| cQOq7wbS0FN1pbx8sb92m7KREL9+wnXuOCHYtublRf7arsMkaZcpSBBaI+raMaZR | ||||
| dUC+LmalIvR8+dNegducwWsdE8/Vh+xq97ZbNFlyut3JOvfuHmaAOvUsX/4touj2 | ||||
| dDkJmvzvmpTBG888t+6hv9eKWaacsTAKuPLThRBD53coTEvHK8iic9fOok65y5Bn | ||||
| nFP/irUpAoGBAPUsPoAPwcNajZX/E4XeG/2eV+IHMxYR9gJwBzpoUfwHr5mR54HK | ||||
| POia/z7/M2KVV9kMWBlbTumIHSEylEEDCCKNNIe1gHBxQ7uGuaf+vVXpVgjBigUz | ||||
| 7oiCjb5RdjevfiyudX/z0B9IQSI9djCXebifEHKpUxAOmU3oP0SEMULLAoGBAOBh | ||||
| G+fDouMU7QN93NG0hssu44yc7xQhb7VLB+isrEQASVG2xWkd3iDrdRxwHAHZqxFd | ||||
| 4DRzDTFC7yeR+FEbVVkWQRaeDwFJM1zmRTXsYjBkK49GNzB4QEtHvPouuxMAQ4UD | ||||
| zJ9a3LEDSs867R7XEbNF8j9NC9z0vk7w9bHTA1aTAoGBAODUUQBY8sQ1zy8lOf8B | ||||
| /sMmKMti9Mshb2su1sIOFli7p6F5tkZEcnSQZs+bccDO2T92XXfrTsNDigr+egvg | ||||
| Pt6IhQqKPB1hEM7wLmLLbU9Sag4fhXVd+TmAF4HW7EUGjvtkhOXwbQOy2+ANYswO | ||||
| rJXMcGXltwE7kgRqnVI0s4PfAoGBALUrM5Dq0bZwyv6qvYVFMiEUdv6uKAwlA0Fq | ||||
| l7Qy19UANjMYVEUPrK7/7stLahHEYu/e0I0I6HoCBX/5yHoUi9Emut88N/ld1W8J | ||||
| LpDfkFhqSRGiLCWisqcWAWwwFzS8XcgkzS9N+iui8OBqP9NK7CvIKlUaLJ33r0Gm | ||||
| JXuzWVqpAoGBAIQ8+YuvFfyhwXuWwQbzxVpWRURH0FRu8KfFIkFFbdyht6leYXuj | ||||
| uxcrcHWzkEPSLO22BoJX8ECHq4LadAIjkkpr5GCikKCF+r/bq50RnECqvfoJ629J | ||||
| gA87C8cLU3dXmSYd+vSg6icZyncTmXyyEV0dqoUGJ2M33kE6hYAbc/ic | ||||
| -----END RSA PRIVATE KEY----- | ||||
|  | @ -1,30 +0,0 @@ | |||
| package(default_visibility = ["//visibility:public"]) | ||||
| 
 | ||||
| load( | ||||
|     "@io_bazel_rules_go//go:def.bzl", | ||||
|     "go_library", | ||||
| ) | ||||
| 
 | ||||
| go_library( | ||||
|     name = "go_default_library", | ||||
|     srcs = ["provider.go"], | ||||
|     importpath = "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/testing", | ||||
|     deps = [ | ||||
|         "//vendor/github.com/coreos/go-oidc/jose:go_default_library", | ||||
|         "//vendor/github.com/coreos/go-oidc/key:go_default_library", | ||||
|         "//vendor/github.com/coreos/go-oidc/oidc:go_default_library", | ||||
|     ], | ||||
| ) | ||||
| 
 | ||||
| filegroup( | ||||
|     name = "package-srcs", | ||||
|     srcs = glob(["**"]), | ||||
|     tags = ["automanaged"], | ||||
|     visibility = ["//visibility:private"], | ||||
| ) | ||||
| 
 | ||||
| filegroup( | ||||
|     name = "all-srcs", | ||||
|     srcs = [":package-srcs"], | ||||
|     tags = ["automanaged"], | ||||
| ) | ||||
|  | @ -1,200 +0,0 @@ | |||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package testing | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"crypto/x509/pkix" | ||||
| 	"encoding/json" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"math/big" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| 	"github.com/coreos/go-oidc/key" | ||||
| 	"github.com/coreos/go-oidc/oidc" | ||||
| ) | ||||
| 
 | ||||
| // NewOIDCProvider provides a bare minimum OIDC IdP Server useful for testing.
 | ||||
| func NewOIDCProvider(t *testing.T, issuerPath string) *OIDCProvider { | ||||
| 	privKey, err := key.GeneratePrivateKey() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Cannot create OIDC Provider: %v", err) | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	op := &OIDCProvider{ | ||||
| 		Mux:        http.NewServeMux(), | ||||
| 		PrivKey:    privKey, | ||||
| 		issuerPath: issuerPath, | ||||
| 	} | ||||
| 
 | ||||
| 	op.Mux.HandleFunc(path.Join(issuerPath, "/.well-known/openid-configuration"), op.handleConfig) | ||||
| 	op.Mux.HandleFunc(path.Join(issuerPath, "/keys"), op.handleKeys) | ||||
| 
 | ||||
| 	return op | ||||
| } | ||||
| 
 | ||||
| type OIDCProvider struct { | ||||
| 	Mux        *http.ServeMux | ||||
| 	PCFG       oidc.ProviderConfig | ||||
| 	PrivKey    *key.PrivateKey | ||||
| 	issuerPath string | ||||
| } | ||||
| 
 | ||||
| func (op *OIDCProvider) ServeTLSWithKeyPair(cert, key string) (*httptest.Server, error) { | ||||
| 	srv := httptest.NewUnstartedServer(op.Mux) | ||||
| 
 | ||||
| 	srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)} | ||||
| 	var err error | ||||
| 	srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("Cannot load cert/key pair: %v", err) | ||||
| 	} | ||||
| 	srv.StartTLS() | ||||
| 
 | ||||
| 	// The issuer's URL is extended by an optional path. This ensures that the plugin can
 | ||||
| 	// handle issuers that use a non-root path for discovery (see kubernetes/kubernetes#29749).
 | ||||
| 	srv.URL = srv.URL + op.issuerPath | ||||
| 
 | ||||
| 	u, err := url.Parse(srv.URL) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	pathFor := func(p string) *url.URL { | ||||
| 		u2 := *u // Shallow copy.
 | ||||
| 		u2.Path = path.Join(u2.Path, p) | ||||
| 		return &u2 | ||||
| 	} | ||||
| 
 | ||||
| 	op.PCFG = oidc.ProviderConfig{ | ||||
| 		Issuer:                  u, | ||||
| 		AuthEndpoint:            pathFor("/auth"), | ||||
| 		TokenEndpoint:           pathFor("/token"), | ||||
| 		KeysEndpoint:            pathFor("/keys"), | ||||
| 		ResponseTypesSupported:  []string{"code"}, | ||||
| 		SubjectTypesSupported:   []string{"public"}, | ||||
| 		IDTokenSigningAlgValues: []string{"RS256"}, | ||||
| 	} | ||||
| 	return srv, nil | ||||
| } | ||||
| 
 | ||||
| func (op *OIDCProvider) handleConfig(w http.ResponseWriter, req *http.Request) { | ||||
| 	b, err := json.Marshal(&op.PCFG) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 	w.Header().Set("Content-Type", "application/json") | ||||
| 	w.Write(b) | ||||
| } | ||||
| 
 | ||||
| func (op *OIDCProvider) handleKeys(w http.ResponseWriter, req *http.Request) { | ||||
| 	keys := struct { | ||||
| 		Keys []jose.JWK `json:"keys"` | ||||
| 	}{ | ||||
| 		Keys: []jose.JWK{op.PrivKey.JWK()}, | ||||
| 	} | ||||
| 
 | ||||
| 	b, err := json.Marshal(keys) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(time.Hour.Seconds()))) | ||||
| 	w.Header().Set("Expires", time.Now().Add(time.Hour).Format(time.RFC1123)) | ||||
| 	w.Header().Set("Content-Type", "application/json") | ||||
| 	w.Write(b) | ||||
| } | ||||
| 
 | ||||
| // generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
 | ||||
| // This method is mostly identical to crypto.GenerateSelfSignedCert except for the 'IsCA' and 'KeyUsage'
 | ||||
| // in the certificate template. (Maybe we can merge these two methods).
 | ||||
| func GenerateSelfSignedCert(t *testing.T, host, certPath, keyPath string) { | ||||
| 	priv, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	template := x509.Certificate{ | ||||
| 		SerialNumber: big.NewInt(1), | ||||
| 		Subject: pkix.Name{ | ||||
| 			CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()), | ||||
| 		}, | ||||
| 		NotBefore: time.Now(), | ||||
| 		NotAfter:  time.Now().Add(time.Hour * 24 * 365), | ||||
| 
 | ||||
| 		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, | ||||
| 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||||
| 		BasicConstraintsValid: true, | ||||
| 		IsCA: true, | ||||
| 	} | ||||
| 
 | ||||
| 	if ip := net.ParseIP(host); ip != nil { | ||||
| 		template.IPAddresses = append(template.IPAddresses, ip) | ||||
| 	} else { | ||||
| 		template.DNSNames = append(template.DNSNames, host) | ||||
| 	} | ||||
| 
 | ||||
| 	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Generate cert
 | ||||
| 	certBuffer := bytes.Buffer{} | ||||
| 	if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Generate key
 | ||||
| 	keyBuffer := bytes.Buffer{} | ||||
| 	if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Write cert
 | ||||
| 	if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if err := ioutil.WriteFile(certPath, certBuffer.Bytes(), os.FileMode(0644)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Write key
 | ||||
| 	if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if err := ioutil.WriteFile(keyPath, keyBuffer.Bytes(), os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,2 @@ | |||
| /bin | ||||
| /gopath | ||||
|  | @ -0,0 +1,16 @@ | |||
| language: go | ||||
| 
 | ||||
| go: | ||||
|   - 1.7.5 | ||||
|   - 1.8 | ||||
| 
 | ||||
| install: | ||||
|  - go get -v -t github.com/coreos/go-oidc/... | ||||
|  - go get golang.org/x/tools/cmd/cover | ||||
|  - go get github.com/golang/lint/golint | ||||
| 
 | ||||
| script: | ||||
|  - ./test | ||||
| 
 | ||||
| notifications: | ||||
|   email: false | ||||
|  | @ -0,0 +1,71 @@ | |||
| # How to Contribute | ||||
| 
 | ||||
| CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via | ||||
| GitHub pull requests.  This document outlines some of the conventions on | ||||
| development workflow, commit message formatting, contact points and other | ||||
| resources to make it easier to get your contribution accepted. | ||||
| 
 | ||||
| # Certificate of Origin | ||||
| 
 | ||||
| By contributing to this project you agree to the Developer Certificate of | ||||
| Origin (DCO). This document was created by the Linux Kernel community and is a | ||||
| simple statement that you, as a contributor, have the legal right to make the | ||||
| contribution. See the [DCO](DCO) file for details. | ||||
| 
 | ||||
| # Email and Chat | ||||
| 
 | ||||
| The project currently uses the general CoreOS email list and IRC channel: | ||||
| - Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev) | ||||
| - IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org | ||||
| 
 | ||||
| Please avoid emailing maintainers found in the MAINTAINERS file directly. They | ||||
| are very busy and read the mailing lists. | ||||
| 
 | ||||
| ## Getting Started | ||||
| 
 | ||||
| - Fork the repository on GitHub | ||||
| - Read the [README](README.md) for build and test instructions | ||||
| - Play with the project, submit bugs, submit patches! | ||||
| 
 | ||||
| ## Contribution Flow | ||||
| 
 | ||||
| This is a rough outline of what a contributor's workflow looks like: | ||||
| 
 | ||||
| - Create a topic branch from where you want to base your work (usually master). | ||||
| - Make commits of logical units. | ||||
| - Make sure your commit messages are in the proper format (see below). | ||||
| - Push your changes to a topic branch in your fork of the repository. | ||||
| - Make sure the tests pass, and add any new tests as appropriate. | ||||
| - Submit a pull request to the original repository. | ||||
| 
 | ||||
| Thanks for your contributions! | ||||
| 
 | ||||
| ### Format of the Commit Message | ||||
| 
 | ||||
| We follow a rough convention for commit messages that is designed to answer two | ||||
| questions: what changed and why. The subject line should feature the what and | ||||
| the body of the commit should describe the why. | ||||
| 
 | ||||
| ``` | ||||
| scripts: add the test-cluster command | ||||
| 
 | ||||
| this uses tmux to setup a test cluster that you can easily kill and | ||||
| start for debugging. | ||||
| 
 | ||||
| Fixes #38 | ||||
| ``` | ||||
| 
 | ||||
| The format can be described more formally as follows: | ||||
| 
 | ||||
| ``` | ||||
| <subsystem>: <what changed> | ||||
| <BLANK LINE> | ||||
| <why this change was made> | ||||
| <BLANK LINE> | ||||
| <footer> | ||||
| ``` | ||||
| 
 | ||||
| The first line is the subject and should be no longer than 70 characters, the | ||||
| second line is always blank, and other lines should be wrapped at 80 characters. | ||||
| This allows the message to be easier to read on GitHub as well as in various | ||||
| git tools. | ||||
|  | @ -0,0 +1,36 @@ | |||
| Developer Certificate of Origin | ||||
| Version 1.1 | ||||
| 
 | ||||
| Copyright (C) 2004, 2006 The Linux Foundation and its contributors. | ||||
| 660 York Street, Suite 102, | ||||
| San Francisco, CA 94110 USA | ||||
| 
 | ||||
| Everyone is permitted to copy and distribute verbatim copies of this | ||||
| license document, but changing it is not allowed. | ||||
| 
 | ||||
| 
 | ||||
| Developer's Certificate of Origin 1.1 | ||||
| 
 | ||||
| By making a contribution to this project, I certify that: | ||||
| 
 | ||||
| (a) The contribution was created in whole or in part by me and I | ||||
|     have the right to submit it under the open source license | ||||
|     indicated in the file; or | ||||
| 
 | ||||
| (b) The contribution is based upon previous work that, to the best | ||||
|     of my knowledge, is covered under an appropriate open source | ||||
|     license and I have the right under that license to submit that | ||||
|     work with modifications, whether created in whole or in part | ||||
|     by me, under the same open source license (unless I am | ||||
|     permitted to submit under a different license), as indicated | ||||
|     in the file; or | ||||
| 
 | ||||
| (c) The contribution was provided directly to me by some other | ||||
|     person who certified (a), (b) or (c) and I have not modified | ||||
|     it. | ||||
| 
 | ||||
| (d) I understand and agree that this project and the contribution | ||||
|     are public and that a record of the contribution (including all | ||||
|     personal information I submit with it, including my sign-off) is | ||||
|     maintained indefinitely and may be redistributed consistent with | ||||
|     this project or the open source license(s) involved. | ||||
|  | @ -0,0 +1,3 @@ | |||
| Bobby Rullo <bobby.rullo@coreos.com> (@bobbyrullo) | ||||
| Ed Rooth <ed.rooth@coreos.com> (@sym3tri) | ||||
| Eric Chiang <eric.chiang@coreos.com> (@ericchiang) | ||||
|  | @ -0,0 +1,72 @@ | |||
| # go-oidc | ||||
| 
 | ||||
| [](https://godoc.org/github.com/coreos/go-oidc) | ||||
| [](https://travis-ci.org/coreos/go-oidc) | ||||
| 
 | ||||
| ## OpenID Connect support for Go | ||||
| 
 | ||||
| This package enables OpenID Connect support for the [golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2) package. | ||||
| 
 | ||||
| ```go | ||||
| provider, err := oidc.NewProvider(ctx, "https://accounts.google.com") | ||||
| if err != nil { | ||||
|     // handle error | ||||
| } | ||||
| 
 | ||||
| // Configure an OpenID Connect aware OAuth2 client. | ||||
| oauth2Config := oauth2.Config{ | ||||
|     ClientID:     clientID, | ||||
|     ClientSecret: clientSecret, | ||||
|     RedirectURL:  redirectURL, | ||||
| 
 | ||||
|     // Discovery returns the OAuth2 endpoints. | ||||
|     Endpoint: provider.Endpoint(), | ||||
| 
 | ||||
|     // "openid" is a required scope for OpenID Connect flows. | ||||
|     Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| OAuth2 redirects are unchanged. | ||||
| 
 | ||||
| ```go | ||||
| func handleRedirect(w http.ResponseWriter, r *http.Request) { | ||||
|     http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| The on responses, the provider can be used to verify ID Tokens. | ||||
| 
 | ||||
| ```go | ||||
| var verifier = provider.Verifier(&oidc.Config{ClientID: clientID}) | ||||
| 
 | ||||
| func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { | ||||
|     // Verify state and errors. | ||||
| 
 | ||||
|     oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code")) | ||||
|     if err != nil { | ||||
|         // handle error | ||||
|     } | ||||
| 
 | ||||
|     // Extract the ID Token from OAuth2 token. | ||||
|     rawIDToken, ok := oauth2Token.Extra("id_token").(string) | ||||
|     if !ok { | ||||
|         // handle missing token | ||||
|     } | ||||
| 
 | ||||
|     // Parse and verify ID Token payload. | ||||
|     idToken, err := verifier.Verify(ctx, rawIDToken) | ||||
|     if err != nil { | ||||
|         // handle error | ||||
|     } | ||||
| 
 | ||||
|     // Extract custom claims | ||||
|     var claims struct { | ||||
|         Email    string `json:"email"` | ||||
|         Verified bool   `json:"email_verified"` | ||||
|     } | ||||
|     if err := idToken.Claims(&claims); err != nil { | ||||
|         // handle error | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | @ -0,0 +1,61 @@ | |||
| ## CoreOS Community Code of Conduct | ||||
| 
 | ||||
| ### Contributor Code of Conduct | ||||
| 
 | ||||
| As contributors and maintainers of this project, and in the interest of | ||||
| fostering an open and welcoming community, we pledge to respect all people who | ||||
| contribute through reporting issues, posting feature requests, updating | ||||
| documentation, submitting pull requests or patches, and other activities. | ||||
| 
 | ||||
| We are committed to making participation in this project a harassment-free | ||||
| experience for everyone, regardless of level of experience, gender, gender | ||||
| identity and expression, sexual orientation, disability, personal appearance, | ||||
| body size, race, ethnicity, age, religion, or nationality. | ||||
| 
 | ||||
| Examples of unacceptable behavior by participants include: | ||||
| 
 | ||||
| * The use of sexualized language or imagery | ||||
| * Personal attacks | ||||
| * Trolling or insulting/derogatory comments | ||||
| * Public or private harassment | ||||
| * Publishing others' private information, such as physical or electronic addresses, without explicit permission | ||||
| * Other unethical or unprofessional conduct. | ||||
| 
 | ||||
| Project maintainers have the right and responsibility to remove, edit, or | ||||
| reject comments, commits, code, wiki edits, issues, and other contributions | ||||
| that are not aligned to this Code of Conduct. By adopting this Code of Conduct, | ||||
| project maintainers commit themselves to fairly and consistently applying these | ||||
| principles to every aspect of managing this project. Project maintainers who do | ||||
| not follow or enforce the Code of Conduct may be permanently removed from the | ||||
| project team. | ||||
| 
 | ||||
| This code of conduct applies both within project spaces and in public spaces | ||||
| when an individual is representing the project or its community. | ||||
| 
 | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported by contacting a project maintainer, Brandon Philips | ||||
| <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>. | ||||
| 
 | ||||
| This Code of Conduct is adapted from the Contributor Covenant | ||||
| (http://contributor-covenant.org), version 1.2.0, available at | ||||
| http://contributor-covenant.org/version/1/2/0/ | ||||
| 
 | ||||
| ### CoreOS Events Code of Conduct | ||||
| 
 | ||||
| CoreOS events are working conferences intended for professional networking and | ||||
| collaboration in the CoreOS community. Attendees are expected to behave | ||||
| according to professional standards and in accordance with their employer’s | ||||
| policies on appropriate workplace behavior. | ||||
| 
 | ||||
| While at CoreOS events or related social networking opportunities, attendees | ||||
| should not engage in discriminatory or offensive speech or actions including | ||||
| but not limited to gender, sexuality, race, age, disability, or religion. | ||||
| Speakers should be especially aware of these concerns. | ||||
| 
 | ||||
| CoreOS does not condone any statements by speakers contrary to these standards. | ||||
| CoreOS reserves the right to deny entrance and/or eject from an event (without | ||||
| refund) any individual found to be engaging in discriminatory or offensive | ||||
| speech or actions. | ||||
| 
 | ||||
| Please bring any concerns to the immediate attention of designated on-site | ||||
| staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>. | ||||
|  | @ -1,7 +0,0 @@ | |||
| package http | ||||
| 
 | ||||
| import "net/http" | ||||
| 
 | ||||
| type Client interface { | ||||
| 	Do(*http.Request) (*http.Response, error) | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| // Package http is DEPRECATED. Use net/http instead.
 | ||||
| package http | ||||
|  | @ -1,161 +0,0 @@ | |||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func WriteError(w http.ResponseWriter, code int, msg string) { | ||||
| 	e := struct { | ||||
| 		Error string `json:"error"` | ||||
| 	}{ | ||||
| 		Error: msg, | ||||
| 	} | ||||
| 	b, err := json.Marshal(e) | ||||
| 	if err != nil { | ||||
| 		log.Printf("go-oidc: failed to marshal %#v: %v", e, err) | ||||
| 		code = http.StatusInternalServerError | ||||
| 		b = []byte(`{"error":"server_error"}`) | ||||
| 	} | ||||
| 	w.Header().Set("Content-Type", "application/json") | ||||
| 	w.WriteHeader(code) | ||||
| 	w.Write(b) | ||||
| } | ||||
| 
 | ||||
| // BasicAuth parses a username and password from the request's
 | ||||
| // Authorization header. This was pulled from golang master:
 | ||||
| // https://codereview.appspot.com/76540043
 | ||||
| func BasicAuth(r *http.Request) (username, password string, ok bool) { | ||||
| 	auth := r.Header.Get("Authorization") | ||||
| 	if auth == "" { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !strings.HasPrefix(auth, "Basic ") { | ||||
| 		return | ||||
| 	} | ||||
| 	c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic ")) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	cs := string(c) | ||||
| 	s := strings.IndexByte(cs, ':') | ||||
| 	if s < 0 { | ||||
| 		return | ||||
| 	} | ||||
| 	return cs[:s], cs[s+1:], true | ||||
| } | ||||
| 
 | ||||
| func cacheControlMaxAge(hdr string) (time.Duration, bool, error) { | ||||
| 	for _, field := range strings.Split(hdr, ",") { | ||||
| 		parts := strings.SplitN(strings.TrimSpace(field), "=", 2) | ||||
| 		k := strings.ToLower(strings.TrimSpace(parts[0])) | ||||
| 		if k != "max-age" { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if len(parts) == 1 { | ||||
| 			return 0, false, errors.New("max-age has no value") | ||||
| 		} | ||||
| 
 | ||||
| 		v := strings.TrimSpace(parts[1]) | ||||
| 		if v == "" { | ||||
| 			return 0, false, errors.New("max-age has empty value") | ||||
| 		} | ||||
| 
 | ||||
| 		age, err := strconv.Atoi(v) | ||||
| 		if err != nil { | ||||
| 			return 0, false, err | ||||
| 		} | ||||
| 
 | ||||
| 		if age <= 0 { | ||||
| 			return 0, false, nil | ||||
| 		} | ||||
| 
 | ||||
| 		return time.Duration(age) * time.Second, true, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return 0, false, nil | ||||
| } | ||||
| 
 | ||||
| func expires(date, expires string) (time.Duration, bool, error) { | ||||
| 	if date == "" || expires == "" { | ||||
| 		return 0, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	var te time.Time | ||||
| 	var err error | ||||
| 	if expires == "0" { | ||||
| 		return 0, false, nil | ||||
| 	} | ||||
| 	te, err = time.Parse(time.RFC1123, expires) | ||||
| 	if err != nil { | ||||
| 		return 0, false, err | ||||
| 	} | ||||
| 
 | ||||
| 	td, err := time.Parse(time.RFC1123, date) | ||||
| 	if err != nil { | ||||
| 		return 0, false, err | ||||
| 	} | ||||
| 
 | ||||
| 	ttl := te.Sub(td) | ||||
| 
 | ||||
| 	// headers indicate data already expired, caller should not
 | ||||
| 	// have to care about this case
 | ||||
| 	if ttl <= 0 { | ||||
| 		return 0, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return ttl, true, nil | ||||
| } | ||||
| 
 | ||||
| func Cacheable(hdr http.Header) (time.Duration, bool, error) { | ||||
| 	ttl, ok, err := cacheControlMaxAge(hdr.Get("Cache-Control")) | ||||
| 	if err != nil || ok { | ||||
| 		return ttl, ok, err | ||||
| 	} | ||||
| 
 | ||||
| 	return expires(hdr.Get("Date"), hdr.Get("Expires")) | ||||
| } | ||||
| 
 | ||||
| // MergeQuery appends additional query values to an existing URL.
 | ||||
| func MergeQuery(u url.URL, q url.Values) url.URL { | ||||
| 	uv := u.Query() | ||||
| 	for k, vs := range q { | ||||
| 		for _, v := range vs { | ||||
| 			uv.Add(k, v) | ||||
| 		} | ||||
| 	} | ||||
| 	u.RawQuery = uv.Encode() | ||||
| 	return u | ||||
| } | ||||
| 
 | ||||
| // NewResourceLocation appends a resource id to the end of the requested URL path.
 | ||||
| func NewResourceLocation(reqURL *url.URL, id string) string { | ||||
| 	var u url.URL | ||||
| 	u = *reqURL | ||||
| 	u.Path = path.Join(u.Path, id) | ||||
| 	u.RawQuery = "" | ||||
| 	u.Fragment = "" | ||||
| 	return u.String() | ||||
| } | ||||
| 
 | ||||
| // CopyRequest returns a clone of the provided *http.Request.
 | ||||
| // The returned object is a shallow copy of the struct and a
 | ||||
| // deep copy of its Header field.
 | ||||
| func CopyRequest(r *http.Request) *http.Request { | ||||
| 	r2 := *r | ||||
| 	r2.Header = make(http.Header) | ||||
| 	for k, s := range r.Header { | ||||
| 		r2.Header[k] = s | ||||
| 	} | ||||
| 	return &r2 | ||||
| } | ||||
|  | @ -1,29 +0,0 @@ | |||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net/url" | ||||
| ) | ||||
| 
 | ||||
| // ParseNonEmptyURL checks that a string is a parsable URL which is also not empty
 | ||||
| // since `url.Parse("")` does not return an error. Must contian a scheme and a host.
 | ||||
| func ParseNonEmptyURL(u string) (*url.URL, error) { | ||||
| 	if u == "" { | ||||
| 		return nil, errors.New("url is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	ur, err := url.Parse(u) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if ur.Scheme == "" { | ||||
| 		return nil, errors.New("url scheme is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	if ur.Host == "" { | ||||
| 		return nil, errors.New("url host is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	return ur, nil | ||||
| } | ||||
|  | @ -0,0 +1,20 @@ | |||
| // +build !golint
 | ||||
| 
 | ||||
| // Don't lint this file. We don't want to have to add a comment to each constant.
 | ||||
| 
 | ||||
| package oidc | ||||
| 
 | ||||
| const ( | ||||
| 	// JOSE asymmetric signing algorithm values as defined by RFC 7518
 | ||||
| 	//
 | ||||
| 	// see: https://tools.ietf.org/html/rfc7518#section-3.1
 | ||||
| 	RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
 | ||||
| 	RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
 | ||||
| 	RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
 | ||||
| 	ES256 = "ES256" // ECDSA using P-256 and SHA-256
 | ||||
| 	ES384 = "ES384" // ECDSA using P-384 and SHA-384
 | ||||
| 	ES512 = "ES512" // ECDSA using P-521 and SHA-512
 | ||||
| 	PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
 | ||||
| 	PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
 | ||||
| 	PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
 | ||||
| ) | ||||
|  | @ -1,126 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Claims map[string]interface{} | ||||
| 
 | ||||
| func (c Claims) Add(name string, value interface{}) { | ||||
| 	c[name] = value | ||||
| } | ||||
| 
 | ||||
| func (c Claims) StringClaim(name string) (string, bool, error) { | ||||
| 	cl, ok := c[name] | ||||
| 	if !ok { | ||||
| 		return "", false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	v, ok := cl.(string) | ||||
| 	if !ok { | ||||
| 		return "", false, fmt.Errorf("unable to parse claim as string: %v", name) | ||||
| 	} | ||||
| 
 | ||||
| 	return v, true, nil | ||||
| } | ||||
| 
 | ||||
| func (c Claims) StringsClaim(name string) ([]string, bool, error) { | ||||
| 	cl, ok := c[name] | ||||
| 	if !ok { | ||||
| 		return nil, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if v, ok := cl.([]string); ok { | ||||
| 		return v, true, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// When unmarshaled, []string will become []interface{}.
 | ||||
| 	if v, ok := cl.([]interface{}); ok { | ||||
| 		var ret []string | ||||
| 		for _, vv := range v { | ||||
| 			str, ok := vv.(string) | ||||
| 			if !ok { | ||||
| 				return nil, false, fmt.Errorf("unable to parse claim as string array: %v", name) | ||||
| 			} | ||||
| 			ret = append(ret, str) | ||||
| 		} | ||||
| 		return ret, true, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, false, fmt.Errorf("unable to parse claim as string array: %v", name) | ||||
| } | ||||
| 
 | ||||
| func (c Claims) Int64Claim(name string) (int64, bool, error) { | ||||
| 	cl, ok := c[name] | ||||
| 	if !ok { | ||||
| 		return 0, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	v, ok := cl.(int64) | ||||
| 	if !ok { | ||||
| 		vf, ok := cl.(float64) | ||||
| 		if !ok { | ||||
| 			return 0, false, fmt.Errorf("unable to parse claim as int64: %v", name) | ||||
| 		} | ||||
| 		v = int64(vf) | ||||
| 	} | ||||
| 
 | ||||
| 	return v, true, nil | ||||
| } | ||||
| 
 | ||||
| func (c Claims) Float64Claim(name string) (float64, bool, error) { | ||||
| 	cl, ok := c[name] | ||||
| 	if !ok { | ||||
| 		return 0, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	v, ok := cl.(float64) | ||||
| 	if !ok { | ||||
| 		vi, ok := cl.(int64) | ||||
| 		if !ok { | ||||
| 			return 0, false, fmt.Errorf("unable to parse claim as float64: %v", name) | ||||
| 		} | ||||
| 		v = float64(vi) | ||||
| 	} | ||||
| 
 | ||||
| 	return v, true, nil | ||||
| } | ||||
| 
 | ||||
| func (c Claims) TimeClaim(name string) (time.Time, bool, error) { | ||||
| 	v, ok, err := c.Float64Claim(name) | ||||
| 	if !ok || err != nil { | ||||
| 		return time.Time{}, ok, err | ||||
| 	} | ||||
| 
 | ||||
| 	s := math.Trunc(v) | ||||
| 	ns := (v - s) * math.Pow(10, 9) | ||||
| 	return time.Unix(int64(s), int64(ns)).UTC(), true, nil | ||||
| } | ||||
| 
 | ||||
| func decodeClaims(payload []byte) (Claims, error) { | ||||
| 	var c Claims | ||||
| 	if err := json.Unmarshal(payload, &c); err != nil { | ||||
| 		return nil, fmt.Errorf("malformed JWT claims, unable to decode: %v", err) | ||||
| 	} | ||||
| 	return c, nil | ||||
| } | ||||
| 
 | ||||
| func marshalClaims(c Claims) ([]byte, error) { | ||||
| 	b, err := json.Marshal(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return b, nil | ||||
| } | ||||
| 
 | ||||
| func encodeClaims(c Claims) (string, error) { | ||||
| 	b, err := marshalClaims(c) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return encodeSegment(b), nil | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| // Package jose is DEPRECATED. Use gopkg.in/square/go-jose.v2 instead.
 | ||||
| package jose | ||||
|  | @ -1,112 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	HeaderMediaType    = "typ" | ||||
| 	HeaderKeyAlgorithm = "alg" | ||||
| 	HeaderKeyID        = "kid" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// Encryption Algorithm Header Parameter Values for JWS
 | ||||
| 	// See: https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#page-6
 | ||||
| 	AlgHS256 = "HS256" | ||||
| 	AlgHS384 = "HS384" | ||||
| 	AlgHS512 = "HS512" | ||||
| 	AlgRS256 = "RS256" | ||||
| 	AlgRS384 = "RS384" | ||||
| 	AlgRS512 = "RS512" | ||||
| 	AlgES256 = "ES256" | ||||
| 	AlgES384 = "ES384" | ||||
| 	AlgES512 = "ES512" | ||||
| 	AlgPS256 = "PS256" | ||||
| 	AlgPS384 = "PS384" | ||||
| 	AlgPS512 = "PS512" | ||||
| 	AlgNone  = "none" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// Algorithm Header Parameter Values for JWE
 | ||||
| 	// See: https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-4.1
 | ||||
| 	AlgRSA15            = "RSA1_5" | ||||
| 	AlgRSAOAEP          = "RSA-OAEP" | ||||
| 	AlgRSAOAEP256       = "RSA-OAEP-256" | ||||
| 	AlgA128KW           = "A128KW" | ||||
| 	AlgA192KW           = "A192KW" | ||||
| 	AlgA256KW           = "A256KW" | ||||
| 	AlgDir              = "dir" | ||||
| 	AlgECDHES           = "ECDH-ES" | ||||
| 	AlgECDHESA128KW     = "ECDH-ES+A128KW" | ||||
| 	AlgECDHESA192KW     = "ECDH-ES+A192KW" | ||||
| 	AlgECDHESA256KW     = "ECDH-ES+A256KW" | ||||
| 	AlgA128GCMKW        = "A128GCMKW" | ||||
| 	AlgA192GCMKW        = "A192GCMKW" | ||||
| 	AlgA256GCMKW        = "A256GCMKW" | ||||
| 	AlgPBES2HS256A128KW = "PBES2-HS256+A128KW" | ||||
| 	AlgPBES2HS384A192KW = "PBES2-HS384+A192KW" | ||||
| 	AlgPBES2HS512A256KW = "PBES2-HS512+A256KW" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// Encryption Algorithm Header Parameter Values for JWE
 | ||||
| 	// See: https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#page-22
 | ||||
| 	EncA128CBCHS256 = "A128CBC-HS256" | ||||
| 	EncA128CBCHS384 = "A128CBC-HS384" | ||||
| 	EncA256CBCHS512 = "A256CBC-HS512" | ||||
| 	EncA128GCM      = "A128GCM" | ||||
| 	EncA192GCM      = "A192GCM" | ||||
| 	EncA256GCM      = "A256GCM" | ||||
| ) | ||||
| 
 | ||||
| type JOSEHeader map[string]string | ||||
| 
 | ||||
| func (j JOSEHeader) Validate() error { | ||||
| 	if _, exists := j[HeaderKeyAlgorithm]; !exists { | ||||
| 		return fmt.Errorf("header missing %q parameter", HeaderKeyAlgorithm) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func decodeHeader(seg string) (JOSEHeader, error) { | ||||
| 	b, err := decodeSegment(seg) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var h JOSEHeader | ||||
| 	err = json.Unmarshal(b, &h) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return h, nil | ||||
| } | ||||
| 
 | ||||
| func encodeHeader(h JOSEHeader) (string, error) { | ||||
| 	b, err := json.Marshal(h) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return encodeSegment(b), nil | ||||
| } | ||||
| 
 | ||||
| // Decode JWT specific base64url encoding with padding stripped
 | ||||
| func decodeSegment(seg string) ([]byte, error) { | ||||
| 	if l := len(seg) % 4; l != 0 { | ||||
| 		seg += strings.Repeat("=", 4-l) | ||||
| 	} | ||||
| 	return base64.URLEncoding.DecodeString(seg) | ||||
| } | ||||
| 
 | ||||
| // Encode JWT specific base64url encoding with padding stripped
 | ||||
| func encodeSegment(seg []byte) string { | ||||
| 	return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=") | ||||
| } | ||||
|  | @ -1,135 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/binary" | ||||
| 	"encoding/json" | ||||
| 	"math/big" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // JSON Web Key
 | ||||
| // https://tools.ietf.org/html/draft-ietf-jose-json-web-key-36#page-5
 | ||||
| type JWK struct { | ||||
| 	ID       string | ||||
| 	Type     string | ||||
| 	Alg      string | ||||
| 	Use      string | ||||
| 	Exponent int | ||||
| 	Modulus  *big.Int | ||||
| 	Secret   []byte | ||||
| } | ||||
| 
 | ||||
| type jwkJSON struct { | ||||
| 	ID       string `json:"kid"` | ||||
| 	Type     string `json:"kty"` | ||||
| 	Alg      string `json:"alg"` | ||||
| 	Use      string `json:"use"` | ||||
| 	Exponent string `json:"e"` | ||||
| 	Modulus  string `json:"n"` | ||||
| } | ||||
| 
 | ||||
| func (j *JWK) MarshalJSON() ([]byte, error) { | ||||
| 	t := jwkJSON{ | ||||
| 		ID:       j.ID, | ||||
| 		Type:     j.Type, | ||||
| 		Alg:      j.Alg, | ||||
| 		Use:      j.Use, | ||||
| 		Exponent: encodeExponent(j.Exponent), | ||||
| 		Modulus:  encodeModulus(j.Modulus), | ||||
| 	} | ||||
| 
 | ||||
| 	return json.Marshal(&t) | ||||
| } | ||||
| 
 | ||||
| func (j *JWK) UnmarshalJSON(data []byte) error { | ||||
| 	var t jwkJSON | ||||
| 	err := json.Unmarshal(data, &t) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	e, err := decodeExponent(t.Exponent) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	n, err := decodeModulus(t.Modulus) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	j.ID = t.ID | ||||
| 	j.Type = t.Type | ||||
| 	j.Alg = t.Alg | ||||
| 	j.Use = t.Use | ||||
| 	j.Exponent = e | ||||
| 	j.Modulus = n | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type JWKSet struct { | ||||
| 	Keys []JWK `json:"keys"` | ||||
| } | ||||
| 
 | ||||
| func decodeExponent(e string) (int, error) { | ||||
| 	decE, err := decodeBase64URLPaddingOptional(e) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	var eBytes []byte | ||||
| 	if len(decE) < 8 { | ||||
| 		eBytes = make([]byte, 8-len(decE), 8) | ||||
| 		eBytes = append(eBytes, decE...) | ||||
| 	} else { | ||||
| 		eBytes = decE | ||||
| 	} | ||||
| 	eReader := bytes.NewReader(eBytes) | ||||
| 	var E uint64 | ||||
| 	err = binary.Read(eReader, binary.BigEndian, &E) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return int(E), nil | ||||
| } | ||||
| 
 | ||||
| func encodeExponent(e int) string { | ||||
| 	b := make([]byte, 8) | ||||
| 	binary.BigEndian.PutUint64(b, uint64(e)) | ||||
| 	var idx int | ||||
| 	for ; idx < 8; idx++ { | ||||
| 		if b[idx] != 0x0 { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	return base64.RawURLEncoding.EncodeToString(b[idx:]) | ||||
| } | ||||
| 
 | ||||
| // Turns a URL encoded modulus of a key into a big int.
 | ||||
| func decodeModulus(n string) (*big.Int, error) { | ||||
| 	decN, err := decodeBase64URLPaddingOptional(n) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	N := big.NewInt(0) | ||||
| 	N.SetBytes(decN) | ||||
| 	return N, nil | ||||
| } | ||||
| 
 | ||||
| func encodeModulus(n *big.Int) string { | ||||
| 	return base64.RawURLEncoding.EncodeToString(n.Bytes()) | ||||
| } | ||||
| 
 | ||||
| // decodeBase64URLPaddingOptional decodes Base64 whether there is padding or not.
 | ||||
| // The stdlib version currently doesn't handle this.
 | ||||
| // We can get rid of this is if this bug:
 | ||||
| //   https://github.com/golang/go/issues/4237
 | ||||
| // ever closes.
 | ||||
| func decodeBase64URLPaddingOptional(e string) ([]byte, error) { | ||||
| 	if m := len(e) % 4; m != 0 { | ||||
| 		e += strings.Repeat("=", 4-m) | ||||
| 	} | ||||
| 	return base64.URLEncoding.DecodeString(e) | ||||
| } | ||||
|  | @ -1,51 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type JWS struct { | ||||
| 	RawHeader  string | ||||
| 	Header     JOSEHeader | ||||
| 	RawPayload string | ||||
| 	Payload    []byte | ||||
| 	Signature  []byte | ||||
| } | ||||
| 
 | ||||
| // Given a raw encoded JWS token parses it and verifies the structure.
 | ||||
| func ParseJWS(raw string) (JWS, error) { | ||||
| 	parts := strings.Split(raw, ".") | ||||
| 	if len(parts) != 3 { | ||||
| 		return JWS{}, fmt.Errorf("malformed JWS, only %d segments", len(parts)) | ||||
| 	} | ||||
| 
 | ||||
| 	rawSig := parts[2] | ||||
| 	jws := JWS{ | ||||
| 		RawHeader:  parts[0], | ||||
| 		RawPayload: parts[1], | ||||
| 	} | ||||
| 
 | ||||
| 	header, err := decodeHeader(jws.RawHeader) | ||||
| 	if err != nil { | ||||
| 		return JWS{}, fmt.Errorf("malformed JWS, unable to decode header, %s", err) | ||||
| 	} | ||||
| 	if err = header.Validate(); err != nil { | ||||
| 		return JWS{}, fmt.Errorf("malformed JWS, %s", err) | ||||
| 	} | ||||
| 	jws.Header = header | ||||
| 
 | ||||
| 	payload, err := decodeSegment(jws.RawPayload) | ||||
| 	if err != nil { | ||||
| 		return JWS{}, fmt.Errorf("malformed JWS, unable to decode payload: %s", err) | ||||
| 	} | ||||
| 	jws.Payload = payload | ||||
| 
 | ||||
| 	sig, err := decodeSegment(rawSig) | ||||
| 	if err != nil { | ||||
| 		return JWS{}, fmt.Errorf("malformed JWS, unable to decode signature: %s", err) | ||||
| 	} | ||||
| 	jws.Signature = sig | ||||
| 
 | ||||
| 	return jws, nil | ||||
| } | ||||
|  | @ -1,82 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import "strings" | ||||
| 
 | ||||
| type JWT JWS | ||||
| 
 | ||||
| func ParseJWT(token string) (jwt JWT, err error) { | ||||
| 	jws, err := ParseJWS(token) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return JWT(jws), nil | ||||
| } | ||||
| 
 | ||||
| func NewJWT(header JOSEHeader, claims Claims) (jwt JWT, err error) { | ||||
| 	jwt = JWT{} | ||||
| 
 | ||||
| 	jwt.Header = header | ||||
| 	jwt.Header[HeaderMediaType] = "JWT" | ||||
| 
 | ||||
| 	claimBytes, err := marshalClaims(claims) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	jwt.Payload = claimBytes | ||||
| 
 | ||||
| 	eh, err := encodeHeader(header) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	jwt.RawHeader = eh | ||||
| 
 | ||||
| 	ec, err := encodeClaims(claims) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	jwt.RawPayload = ec | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (j *JWT) KeyID() (string, bool) { | ||||
| 	kID, ok := j.Header[HeaderKeyID] | ||||
| 	return kID, ok | ||||
| } | ||||
| 
 | ||||
| func (j *JWT) Claims() (Claims, error) { | ||||
| 	return decodeClaims(j.Payload) | ||||
| } | ||||
| 
 | ||||
| // Encoded data part of the token which may be signed.
 | ||||
| func (j *JWT) Data() string { | ||||
| 	return strings.Join([]string{j.RawHeader, j.RawPayload}, ".") | ||||
| } | ||||
| 
 | ||||
| // Full encoded JWT token string in format: header.claims.signature
 | ||||
| func (j *JWT) Encode() string { | ||||
| 	d := j.Data() | ||||
| 	s := encodeSegment(j.Signature) | ||||
| 	return strings.Join([]string{d, s}, ".") | ||||
| } | ||||
| 
 | ||||
| func NewSignedJWT(claims Claims, s Signer) (*JWT, error) { | ||||
| 	header := JOSEHeader{ | ||||
| 		HeaderKeyAlgorithm: s.Alg(), | ||||
| 		HeaderKeyID:        s.ID(), | ||||
| 	} | ||||
| 
 | ||||
| 	jwt, err := NewJWT(header, claims) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sig, err := s.Sign([]byte(jwt.Data())) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	jwt.Signature = sig | ||||
| 
 | ||||
| 	return &jwt, nil | ||||
| } | ||||
|  | @ -1,24 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| ) | ||||
| 
 | ||||
| type Verifier interface { | ||||
| 	ID() string | ||||
| 	Alg() string | ||||
| 	Verify(sig []byte, data []byte) error | ||||
| } | ||||
| 
 | ||||
| type Signer interface { | ||||
| 	Verifier | ||||
| 	Sign(data []byte) (sig []byte, err error) | ||||
| } | ||||
| 
 | ||||
| func NewVerifier(jwk JWK) (Verifier, error) { | ||||
| 	if jwk.Type != "RSA" { | ||||
| 		return nil, fmt.Errorf("unsupported key type %q", jwk.Type) | ||||
| 	} | ||||
| 
 | ||||
| 	return NewVerifierRSA(jwk) | ||||
| } | ||||
|  | @ -1,67 +0,0 @@ | |||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"fmt" | ||||
| ) | ||||
| 
 | ||||
| type VerifierRSA struct { | ||||
| 	KeyID     string | ||||
| 	Hash      crypto.Hash | ||||
| 	PublicKey rsa.PublicKey | ||||
| } | ||||
| 
 | ||||
| type SignerRSA struct { | ||||
| 	PrivateKey rsa.PrivateKey | ||||
| 	VerifierRSA | ||||
| } | ||||
| 
 | ||||
| func NewVerifierRSA(jwk JWK) (*VerifierRSA, error) { | ||||
| 	if jwk.Alg != "" && jwk.Alg != "RS256" { | ||||
| 		return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg) | ||||
| 	} | ||||
| 
 | ||||
| 	v := VerifierRSA{ | ||||
| 		KeyID: jwk.ID, | ||||
| 		PublicKey: rsa.PublicKey{ | ||||
| 			N: jwk.Modulus, | ||||
| 			E: jwk.Exponent, | ||||
| 		}, | ||||
| 		Hash: crypto.SHA256, | ||||
| 	} | ||||
| 
 | ||||
| 	return &v, nil | ||||
| } | ||||
| 
 | ||||
| func NewSignerRSA(kid string, key rsa.PrivateKey) *SignerRSA { | ||||
| 	return &SignerRSA{ | ||||
| 		PrivateKey: key, | ||||
| 		VerifierRSA: VerifierRSA{ | ||||
| 			KeyID:     kid, | ||||
| 			PublicKey: key.PublicKey, | ||||
| 			Hash:      crypto.SHA256, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (v *VerifierRSA) ID() string { | ||||
| 	return v.KeyID | ||||
| } | ||||
| 
 | ||||
| func (v *VerifierRSA) Alg() string { | ||||
| 	return "RS256" | ||||
| } | ||||
| 
 | ||||
| func (v *VerifierRSA) Verify(sig []byte, data []byte) error { | ||||
| 	h := v.Hash.New() | ||||
| 	h.Write(data) | ||||
| 	return rsa.VerifyPKCS1v15(&v.PublicKey, v.Hash, h.Sum(nil), sig) | ||||
| } | ||||
| 
 | ||||
| func (s *SignerRSA) Sign(data []byte) ([]byte, error) { | ||||
| 	h := s.Hash.New() | ||||
| 	h.Write(data) | ||||
| 	return rsa.SignPKCS1v15(rand.Reader, &s.PrivateKey, s.Hash, h.Sum(nil)) | ||||
| } | ||||
|  | @ -0,0 +1,228 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/pquerna/cachecontrol" | ||||
| 	jose "gopkg.in/square/go-jose.v2" | ||||
| ) | ||||
| 
 | ||||
| // keysExpiryDelta is the allowed clock skew between a client and the OpenID Connect
 | ||||
| // server.
 | ||||
| //
 | ||||
| // When keys expire, they are valid for this amount of time after.
 | ||||
| //
 | ||||
| // If the keys have not expired, and an ID Token claims it was signed by a key not in
 | ||||
| // the cache, if and only if the keys expire in this amount of time, the keys will be
 | ||||
| // updated.
 | ||||
| const keysExpiryDelta = 30 * time.Second | ||||
| 
 | ||||
| // NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
 | ||||
| // GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
 | ||||
| // used by NewProvider using the URLs returned by OpenID Connect discovery, but is
 | ||||
| // exposed for providers that don't support discovery or to prevent round trips to the
 | ||||
| // discovery URL.
 | ||||
| //
 | ||||
| // The returned KeySet is a long lived verifier that caches keys based on cache-control
 | ||||
| // headers. Reuse a common remote key set instead of creating new ones as needed.
 | ||||
| //
 | ||||
| // The behavior of the returned KeySet is undefined once the context is canceled.
 | ||||
| func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet { | ||||
| 	return newRemoteKeySet(ctx, jwksURL, time.Now) | ||||
| } | ||||
| 
 | ||||
| func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet { | ||||
| 	if now == nil { | ||||
| 		now = time.Now | ||||
| 	} | ||||
| 	return &remoteKeySet{jwksURL: jwksURL, ctx: ctx, now: now} | ||||
| } | ||||
| 
 | ||||
| type remoteKeySet struct { | ||||
| 	jwksURL string | ||||
| 	ctx     context.Context | ||||
| 	now     func() time.Time | ||||
| 
 | ||||
| 	// guard all other fields
 | ||||
| 	mu sync.Mutex | ||||
| 
 | ||||
| 	// inflight suppresses parallel execution of updateKeys and allows
 | ||||
| 	// multiple goroutines to wait for its result.
 | ||||
| 	inflight *inflight | ||||
| 
 | ||||
| 	// A set of cached keys and their expiry.
 | ||||
| 	cachedKeys []jose.JSONWebKey | ||||
| 	expiry     time.Time | ||||
| } | ||||
| 
 | ||||
| // inflight is used to wait on some in-flight request from multiple goroutines.
 | ||||
| type inflight struct { | ||||
| 	doneCh chan struct{} | ||||
| 
 | ||||
| 	keys []jose.JSONWebKey | ||||
| 	err  error | ||||
| } | ||||
| 
 | ||||
| func newInflight() *inflight { | ||||
| 	return &inflight{doneCh: make(chan struct{})} | ||||
| } | ||||
| 
 | ||||
| // wait returns a channel that multiple goroutines can receive on. Once it returns
 | ||||
| // a value, the inflight request is done and result() can be inspected.
 | ||||
| func (i *inflight) wait() <-chan struct{} { | ||||
| 	return i.doneCh | ||||
| } | ||||
| 
 | ||||
| // done can only be called by a single goroutine. It records the result of the
 | ||||
| // inflight request and signals other goroutines that the result is safe to
 | ||||
| // inspect.
 | ||||
| func (i *inflight) done(keys []jose.JSONWebKey, err error) { | ||||
| 	i.keys = keys | ||||
| 	i.err = err | ||||
| 	close(i.doneCh) | ||||
| } | ||||
| 
 | ||||
| // result cannot be called until the wait() channel has returned a value.
 | ||||
| func (i *inflight) result() ([]jose.JSONWebKey, error) { | ||||
| 	return i.keys, i.err | ||||
| } | ||||
| 
 | ||||
| func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { | ||||
| 	jws, err := jose.ParseSigned(jwt) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: malformed jwt: %v", err) | ||||
| 	} | ||||
| 	return r.verify(ctx, jws) | ||||
| } | ||||
| 
 | ||||
| func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { | ||||
| 	// We don't support JWTs signed with multiple signatures.
 | ||||
| 	keyID := "" | ||||
| 	for _, sig := range jws.Signatures { | ||||
| 		keyID = sig.Header.KeyID | ||||
| 		break | ||||
| 	} | ||||
| 
 | ||||
| 	keys, expiry := r.keysFromCache() | ||||
| 
 | ||||
| 	// Don't check expiry yet. This optimizes for when the provider is unavailable.
 | ||||
| 	for _, key := range keys { | ||||
| 		if keyID == "" || key.KeyID == keyID { | ||||
| 			if payload, err := jws.Verify(&key); err == nil { | ||||
| 				return payload, nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !r.now().Add(keysExpiryDelta).After(expiry) { | ||||
| 		// Keys haven't expired, don't refresh.
 | ||||
| 		return nil, errors.New("failed to verify id token signature") | ||||
| 	} | ||||
| 
 | ||||
| 	keys, err := r.keysFromRemote(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("fetching keys %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, key := range keys { | ||||
| 		if keyID == "" || key.KeyID == keyID { | ||||
| 			if payload, err := jws.Verify(&key); err == nil { | ||||
| 				return payload, nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil, errors.New("failed to verify id token signature") | ||||
| } | ||||
| 
 | ||||
| func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey, expiry time.Time) { | ||||
| 	r.mu.Lock() | ||||
| 	defer r.mu.Unlock() | ||||
| 	return r.cachedKeys, r.expiry | ||||
| } | ||||
| 
 | ||||
| // keysFromRemote syncs the key set from the remote set, records the values in the
 | ||||
| // cache, and returns the key set.
 | ||||
| func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) { | ||||
| 	// Need to lock to inspect the inflight request field.
 | ||||
| 	r.mu.Lock() | ||||
| 	// If there's not a current inflight request, create one.
 | ||||
| 	if r.inflight == nil { | ||||
| 		r.inflight = newInflight() | ||||
| 
 | ||||
| 		// This goroutine has exclusive ownership over the current inflight
 | ||||
| 		// request. It releases the resource by nil'ing the inflight field
 | ||||
| 		// once the goroutine is done.
 | ||||
| 		go func() { | ||||
| 			// Sync keys and finish inflight when that's done.
 | ||||
| 			keys, expiry, err := r.updateKeys() | ||||
| 
 | ||||
| 			r.inflight.done(keys, err) | ||||
| 
 | ||||
| 			// Lock to update the keys and indicate that there is no longer an
 | ||||
| 			// inflight request.
 | ||||
| 			r.mu.Lock() | ||||
| 			defer r.mu.Unlock() | ||||
| 
 | ||||
| 			if err == nil { | ||||
| 				r.cachedKeys = keys | ||||
| 				r.expiry = expiry | ||||
| 			} | ||||
| 
 | ||||
| 			// Free inflight so a different request can run.
 | ||||
| 			r.inflight = nil | ||||
| 		}() | ||||
| 	} | ||||
| 	inflight := r.inflight | ||||
| 	r.mu.Unlock() | ||||
| 
 | ||||
| 	select { | ||||
| 	case <-ctx.Done(): | ||||
| 		return nil, ctx.Err() | ||||
| 	case <-inflight.wait(): | ||||
| 		return inflight.result() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (r *remoteKeySet) updateKeys() ([]jose.JSONWebKey, time.Time, error) { | ||||
| 	req, err := http.NewRequest("GET", r.jwksURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, time.Time{}, fmt.Errorf("oidc: can't create request: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := doRequest(r.ctx, req) | ||||
| 	if err != nil { | ||||
| 		return nil, time.Time{}, fmt.Errorf("oidc: get keys failed %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, time.Time{}, fmt.Errorf("unable to read response body: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, time.Time{}, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body) | ||||
| 	} | ||||
| 
 | ||||
| 	var keySet jose.JSONWebKeySet | ||||
| 	err = unmarshalResp(resp, body, &keySet) | ||||
| 	if err != nil { | ||||
| 		return nil, time.Time{}, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body) | ||||
| 	} | ||||
| 
 | ||||
| 	// If the server doesn't provide cache control headers, assume the
 | ||||
| 	// keys expire immediately.
 | ||||
| 	expiry := r.now() | ||||
| 
 | ||||
| 	_, e, err := cachecontrol.CachableResponse(req, resp, cachecontrol.Options{}) | ||||
| 	if err == nil && e.After(expiry) { | ||||
| 		expiry = e | ||||
| 	} | ||||
| 	return keySet.Keys, expiry, nil | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| // Package key is DEPRECATED. Use github.com/coreos/go-oidc instead.
 | ||||
| package key | ||||
|  | @ -1,153 +0,0 @@ | |||
| package key | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"encoding/hex" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| ) | ||||
| 
 | ||||
| func NewPublicKey(jwk jose.JWK) *PublicKey { | ||||
| 	return &PublicKey{jwk: jwk} | ||||
| } | ||||
| 
 | ||||
| type PublicKey struct { | ||||
| 	jwk jose.JWK | ||||
| } | ||||
| 
 | ||||
| func (k *PublicKey) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(&k.jwk) | ||||
| } | ||||
| 
 | ||||
| func (k *PublicKey) UnmarshalJSON(data []byte) error { | ||||
| 	var jwk jose.JWK | ||||
| 	if err := json.Unmarshal(data, &jwk); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	k.jwk = jwk | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (k *PublicKey) ID() string { | ||||
| 	return k.jwk.ID | ||||
| } | ||||
| 
 | ||||
| func (k *PublicKey) Verifier() (jose.Verifier, error) { | ||||
| 	return jose.NewVerifierRSA(k.jwk) | ||||
| } | ||||
| 
 | ||||
| type PrivateKey struct { | ||||
| 	KeyID      string | ||||
| 	PrivateKey *rsa.PrivateKey | ||||
| } | ||||
| 
 | ||||
| func (k *PrivateKey) ID() string { | ||||
| 	return k.KeyID | ||||
| } | ||||
| 
 | ||||
| func (k *PrivateKey) Signer() jose.Signer { | ||||
| 	return jose.NewSignerRSA(k.ID(), *k.PrivateKey) | ||||
| } | ||||
| 
 | ||||
| func (k *PrivateKey) JWK() jose.JWK { | ||||
| 	return jose.JWK{ | ||||
| 		ID:       k.KeyID, | ||||
| 		Type:     "RSA", | ||||
| 		Alg:      "RS256", | ||||
| 		Use:      "sig", | ||||
| 		Exponent: k.PrivateKey.PublicKey.E, | ||||
| 		Modulus:  k.PrivateKey.PublicKey.N, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type KeySet interface { | ||||
| 	ExpiresAt() time.Time | ||||
| } | ||||
| 
 | ||||
| type PublicKeySet struct { | ||||
| 	keys      []PublicKey | ||||
| 	index     map[string]*PublicKey | ||||
| 	expiresAt time.Time | ||||
| } | ||||
| 
 | ||||
| func NewPublicKeySet(jwks []jose.JWK, exp time.Time) *PublicKeySet { | ||||
| 	keys := make([]PublicKey, len(jwks)) | ||||
| 	index := make(map[string]*PublicKey) | ||||
| 	for i, jwk := range jwks { | ||||
| 		keys[i] = *NewPublicKey(jwk) | ||||
| 		index[keys[i].ID()] = &keys[i] | ||||
| 	} | ||||
| 	return &PublicKeySet{ | ||||
| 		keys:      keys, | ||||
| 		index:     index, | ||||
| 		expiresAt: exp, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *PublicKeySet) ExpiresAt() time.Time { | ||||
| 	return s.expiresAt | ||||
| } | ||||
| 
 | ||||
| func (s *PublicKeySet) Keys() []PublicKey { | ||||
| 	return s.keys | ||||
| } | ||||
| 
 | ||||
| func (s *PublicKeySet) Key(id string) *PublicKey { | ||||
| 	return s.index[id] | ||||
| } | ||||
| 
 | ||||
| type PrivateKeySet struct { | ||||
| 	keys        []*PrivateKey | ||||
| 	ActiveKeyID string | ||||
| 	expiresAt   time.Time | ||||
| } | ||||
| 
 | ||||
| func NewPrivateKeySet(keys []*PrivateKey, exp time.Time) *PrivateKeySet { | ||||
| 	return &PrivateKeySet{ | ||||
| 		keys:        keys, | ||||
| 		ActiveKeyID: keys[0].ID(), | ||||
| 		expiresAt:   exp.UTC(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *PrivateKeySet) Keys() []*PrivateKey { | ||||
| 	return s.keys | ||||
| } | ||||
| 
 | ||||
| func (s *PrivateKeySet) ExpiresAt() time.Time { | ||||
| 	return s.expiresAt | ||||
| } | ||||
| 
 | ||||
| func (s *PrivateKeySet) Active() *PrivateKey { | ||||
| 	for i, k := range s.keys { | ||||
| 		if k.ID() == s.ActiveKeyID { | ||||
| 			return s.keys[i] | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type GeneratePrivateKeyFunc func() (*PrivateKey, error) | ||||
| 
 | ||||
| func GeneratePrivateKey() (*PrivateKey, error) { | ||||
| 	pk, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	keyID := make([]byte, 20) | ||||
| 	if _, err := io.ReadFull(rand.Reader, keyID); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	k := PrivateKey{ | ||||
| 		KeyID:      hex.EncodeToString(keyID), | ||||
| 		PrivateKey: pk, | ||||
| 	} | ||||
| 
 | ||||
| 	return &k, nil | ||||
| } | ||||
|  | @ -1,99 +0,0 @@ | |||
| package key | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/jonboulle/clockwork" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| 	"github.com/coreos/pkg/health" | ||||
| ) | ||||
| 
 | ||||
| type PrivateKeyManager interface { | ||||
| 	ExpiresAt() time.Time | ||||
| 	Signer() (jose.Signer, error) | ||||
| 	JWKs() ([]jose.JWK, error) | ||||
| 	PublicKeys() ([]PublicKey, error) | ||||
| 
 | ||||
| 	WritableKeySetRepo | ||||
| 	health.Checkable | ||||
| } | ||||
| 
 | ||||
| func NewPrivateKeyManager() PrivateKeyManager { | ||||
| 	return &privateKeyManager{ | ||||
| 		clock: clockwork.NewRealClock(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type privateKeyManager struct { | ||||
| 	keySet *PrivateKeySet | ||||
| 	clock  clockwork.Clock | ||||
| } | ||||
| 
 | ||||
| func (m *privateKeyManager) ExpiresAt() time.Time { | ||||
| 	if m.keySet == nil { | ||||
| 		return m.clock.Now().UTC() | ||||
| 	} | ||||
| 
 | ||||
| 	return m.keySet.ExpiresAt() | ||||
| } | ||||
| 
 | ||||
| func (m *privateKeyManager) Signer() (jose.Signer, error) { | ||||
| 	if err := m.Healthy(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return m.keySet.Active().Signer(), nil | ||||
| } | ||||
| 
 | ||||
| func (m *privateKeyManager) JWKs() ([]jose.JWK, error) { | ||||
| 	if err := m.Healthy(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	keys := m.keySet.Keys() | ||||
| 	jwks := make([]jose.JWK, len(keys)) | ||||
| 	for i, k := range keys { | ||||
| 		jwks[i] = k.JWK() | ||||
| 	} | ||||
| 	return jwks, nil | ||||
| } | ||||
| 
 | ||||
| func (m *privateKeyManager) PublicKeys() ([]PublicKey, error) { | ||||
| 	jwks, err := m.JWKs() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	keys := make([]PublicKey, len(jwks)) | ||||
| 	for i, jwk := range jwks { | ||||
| 		keys[i] = *NewPublicKey(jwk) | ||||
| 	} | ||||
| 	return keys, nil | ||||
| } | ||||
| 
 | ||||
| func (m *privateKeyManager) Healthy() error { | ||||
| 	if m.keySet == nil { | ||||
| 		return errors.New("private key manager uninitialized") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(m.keySet.Keys()) == 0 { | ||||
| 		return errors.New("private key manager zero keys") | ||||
| 	} | ||||
| 
 | ||||
| 	if m.keySet.ExpiresAt().Before(m.clock.Now().UTC()) { | ||||
| 		return errors.New("private key manager keys expired") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *privateKeyManager) Set(keySet KeySet) error { | ||||
| 	privKeySet, ok := keySet.(*PrivateKeySet) | ||||
| 	if !ok { | ||||
| 		return errors.New("unable to cast to PrivateKeySet") | ||||
| 	} | ||||
| 
 | ||||
| 	m.keySet = privKeySet | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,55 +0,0 @@ | |||
| package key | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| var ErrorNoKeys = errors.New("no keys found") | ||||
| 
 | ||||
| type WritableKeySetRepo interface { | ||||
| 	Set(KeySet) error | ||||
| } | ||||
| 
 | ||||
| type ReadableKeySetRepo interface { | ||||
| 	Get() (KeySet, error) | ||||
| } | ||||
| 
 | ||||
| type PrivateKeySetRepo interface { | ||||
| 	WritableKeySetRepo | ||||
| 	ReadableKeySetRepo | ||||
| } | ||||
| 
 | ||||
| func NewPrivateKeySetRepo() PrivateKeySetRepo { | ||||
| 	return &memPrivateKeySetRepo{} | ||||
| } | ||||
| 
 | ||||
| type memPrivateKeySetRepo struct { | ||||
| 	mu  sync.RWMutex | ||||
| 	pks PrivateKeySet | ||||
| } | ||||
| 
 | ||||
| func (r *memPrivateKeySetRepo) Set(ks KeySet) error { | ||||
| 	pks, ok := ks.(*PrivateKeySet) | ||||
| 	if !ok { | ||||
| 		return errors.New("unable to cast to PrivateKeySet") | ||||
| 	} else if pks == nil { | ||||
| 		return errors.New("nil KeySet") | ||||
| 	} | ||||
| 
 | ||||
| 	r.mu.Lock() | ||||
| 	defer r.mu.Unlock() | ||||
| 
 | ||||
| 	r.pks = *pks | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (r *memPrivateKeySetRepo) Get() (KeySet, error) { | ||||
| 	r.mu.RLock() | ||||
| 	defer r.mu.RUnlock() | ||||
| 
 | ||||
| 	if r.pks.keys == nil { | ||||
| 		return nil, ErrorNoKeys | ||||
| 	} | ||||
| 	return KeySet(&r.pks), nil | ||||
| } | ||||
|  | @ -1,159 +0,0 @@ | |||
| package key | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"log" | ||||
| 	"time" | ||||
| 
 | ||||
| 	ptime "github.com/coreos/pkg/timeutil" | ||||
| 	"github.com/jonboulle/clockwork" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrorPrivateKeysExpired = errors.New("private keys have expired") | ||||
| ) | ||||
| 
 | ||||
| func NewPrivateKeyRotator(repo PrivateKeySetRepo, ttl time.Duration) *PrivateKeyRotator { | ||||
| 	return &PrivateKeyRotator{ | ||||
| 		repo: repo, | ||||
| 		ttl:  ttl, | ||||
| 
 | ||||
| 		keep:        2, | ||||
| 		generateKey: GeneratePrivateKey, | ||||
| 		clock:       clockwork.NewRealClock(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type PrivateKeyRotator struct { | ||||
| 	repo        PrivateKeySetRepo | ||||
| 	generateKey GeneratePrivateKeyFunc | ||||
| 	clock       clockwork.Clock | ||||
| 	keep        int | ||||
| 	ttl         time.Duration | ||||
| } | ||||
| 
 | ||||
| func (r *PrivateKeyRotator) expiresAt() time.Time { | ||||
| 	return r.clock.Now().UTC().Add(r.ttl) | ||||
| } | ||||
| 
 | ||||
| func (r *PrivateKeyRotator) Healthy() error { | ||||
| 	pks, err := r.privateKeySet() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if r.clock.Now().After(pks.ExpiresAt()) { | ||||
| 		return ErrorPrivateKeysExpired | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (r *PrivateKeyRotator) privateKeySet() (*PrivateKeySet, error) { | ||||
| 	ks, err := r.repo.Get() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	pks, ok := ks.(*PrivateKeySet) | ||||
| 	if !ok { | ||||
| 		return nil, errors.New("unable to cast to PrivateKeySet") | ||||
| 	} | ||||
| 	return pks, nil | ||||
| } | ||||
| 
 | ||||
| func (r *PrivateKeyRotator) nextRotation() (time.Duration, error) { | ||||
| 	pks, err := r.privateKeySet() | ||||
| 	if err == ErrorNoKeys { | ||||
| 		return 0, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 
 | ||||
| 	now := r.clock.Now() | ||||
| 
 | ||||
| 	// Ideally, we want to rotate after half the TTL has elapsed.
 | ||||
| 	idealRotationTime := pks.ExpiresAt().Add(-r.ttl / 2) | ||||
| 
 | ||||
| 	// If we are past the ideal rotation time, rotate immediatly.
 | ||||
| 	return max(0, idealRotationTime.Sub(now)), nil | ||||
| } | ||||
| 
 | ||||
| func max(a, b time.Duration) time.Duration { | ||||
| 	if a > b { | ||||
| 		return a | ||||
| 	} | ||||
| 	return b | ||||
| } | ||||
| 
 | ||||
| func (r *PrivateKeyRotator) Run() chan struct{} { | ||||
| 	attempt := func() { | ||||
| 		k, err := r.generateKey() | ||||
| 		if err != nil { | ||||
| 			log.Printf("go-oidc: failed generating signing key: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		exp := r.expiresAt() | ||||
| 		if err := rotatePrivateKeys(r.repo, k, r.keep, exp); err != nil { | ||||
| 			log.Printf("go-oidc: key rotation failed: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	stop := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			var nextRotation time.Duration | ||||
| 			var sleep time.Duration | ||||
| 			var err error | ||||
| 			for { | ||||
| 				if nextRotation, err = r.nextRotation(); err == nil { | ||||
| 					break | ||||
| 				} | ||||
| 				sleep = ptime.ExpBackoff(sleep, time.Minute) | ||||
| 				log.Printf("go-oidc: error getting nextRotation, retrying in %v: %v", sleep, err) | ||||
| 				time.Sleep(sleep) | ||||
| 			} | ||||
| 
 | ||||
| 			select { | ||||
| 			case <-r.clock.After(nextRotation): | ||||
| 				attempt() | ||||
| 			case <-stop: | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	return stop | ||||
| } | ||||
| 
 | ||||
| func rotatePrivateKeys(repo PrivateKeySetRepo, k *PrivateKey, keep int, exp time.Time) error { | ||||
| 	ks, err := repo.Get() | ||||
| 	if err != nil && err != ErrorNoKeys { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	var keys []*PrivateKey | ||||
| 	if ks != nil { | ||||
| 		pks, ok := ks.(*PrivateKeySet) | ||||
| 		if !ok { | ||||
| 			return errors.New("unable to cast to PrivateKeySet") | ||||
| 		} | ||||
| 		keys = pks.Keys() | ||||
| 	} | ||||
| 
 | ||||
| 	keys = append([]*PrivateKey{k}, keys...) | ||||
| 	if l := len(keys); l > keep { | ||||
| 		keys = keys[0:keep] | ||||
| 	} | ||||
| 
 | ||||
| 	nks := PrivateKeySet{ | ||||
| 		keys:        keys, | ||||
| 		ActiveKeyID: k.ID(), | ||||
| 		expiresAt:   exp, | ||||
| 	} | ||||
| 
 | ||||
| 	return repo.Set(KeySet(&nks)) | ||||
| } | ||||
|  | @ -1,91 +0,0 @@ | |||
| package key | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"log" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/jonboulle/clockwork" | ||||
| 
 | ||||
| 	"github.com/coreos/pkg/timeutil" | ||||
| ) | ||||
| 
 | ||||
| func NewKeySetSyncer(r ReadableKeySetRepo, w WritableKeySetRepo) *KeySetSyncer { | ||||
| 	return &KeySetSyncer{ | ||||
| 		readable: r, | ||||
| 		writable: w, | ||||
| 		clock:    clockwork.NewRealClock(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type KeySetSyncer struct { | ||||
| 	readable ReadableKeySetRepo | ||||
| 	writable WritableKeySetRepo | ||||
| 	clock    clockwork.Clock | ||||
| } | ||||
| 
 | ||||
| func (s *KeySetSyncer) Run() chan struct{} { | ||||
| 	stop := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		var failing bool | ||||
| 		var next time.Duration | ||||
| 		for { | ||||
| 			exp, err := syncKeySet(s.readable, s.writable, s.clock) | ||||
| 			if err != nil || exp == 0 { | ||||
| 				if !failing { | ||||
| 					failing = true | ||||
| 					next = time.Second | ||||
| 				} else { | ||||
| 					next = timeutil.ExpBackoff(next, time.Minute) | ||||
| 				} | ||||
| 				if exp == 0 { | ||||
| 					log.Printf("Synced to already expired key set, retrying in %v: %v", next, err) | ||||
| 
 | ||||
| 				} else { | ||||
| 					log.Printf("Failed syncing key set, retrying in %v: %v", next, err) | ||||
| 				} | ||||
| 			} else { | ||||
| 				failing = false | ||||
| 				next = exp / 2 | ||||
| 			} | ||||
| 
 | ||||
| 			select { | ||||
| 			case <-s.clock.After(next): | ||||
| 				continue | ||||
| 			case <-stop: | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	return stop | ||||
| } | ||||
| 
 | ||||
| func Sync(r ReadableKeySetRepo, w WritableKeySetRepo) (time.Duration, error) { | ||||
| 	return syncKeySet(r, w, clockwork.NewRealClock()) | ||||
| } | ||||
| 
 | ||||
| // syncKeySet copies the keyset from r to the KeySet at w and returns the duration in which the KeySet will expire.
 | ||||
| // If keyset has already expired, returns a zero duration.
 | ||||
| func syncKeySet(r ReadableKeySetRepo, w WritableKeySetRepo, clock clockwork.Clock) (exp time.Duration, err error) { | ||||
| 	var ks KeySet | ||||
| 	ks, err = r.Get() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if ks == nil { | ||||
| 		err = errors.New("no source KeySet") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err = w.Set(ks); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	now := clock.Now() | ||||
| 	if ks.ExpiresAt().After(now) { | ||||
| 		exp = ks.ExpiresAt().Sub(now) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| // Package oauth2 is DEPRECATED. Use golang.org/x/oauth instead.
 | ||||
| package oauth2 | ||||
|  | @ -1,29 +0,0 @@ | |||
| package oauth2 | ||||
| 
 | ||||
| const ( | ||||
| 	ErrorAccessDenied            = "access_denied" | ||||
| 	ErrorInvalidClient           = "invalid_client" | ||||
| 	ErrorInvalidGrant            = "invalid_grant" | ||||
| 	ErrorInvalidRequest          = "invalid_request" | ||||
| 	ErrorServerError             = "server_error" | ||||
| 	ErrorUnauthorizedClient      = "unauthorized_client" | ||||
| 	ErrorUnsupportedGrantType    = "unsupported_grant_type" | ||||
| 	ErrorUnsupportedResponseType = "unsupported_response_type" | ||||
| ) | ||||
| 
 | ||||
| type Error struct { | ||||
| 	Type        string `json:"error"` | ||||
| 	Description string `json:"error_description,omitempty"` | ||||
| 	State       string `json:"state,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func (e *Error) Error() string { | ||||
| 	if e.Description != "" { | ||||
| 		return e.Type + ": " + e.Description | ||||
| 	} | ||||
| 	return e.Type | ||||
| } | ||||
| 
 | ||||
| func NewError(typ string) *Error { | ||||
| 	return &Error{Type: typ} | ||||
| } | ||||
|  | @ -1,416 +0,0 @@ | |||
| package oauth2 | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"mime" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	phttp "github.com/coreos/go-oidc/http" | ||||
| ) | ||||
| 
 | ||||
| // ResponseTypesEqual compares two response_type values. If either
 | ||||
| // contains a space, it is treated as an unordered list. For example,
 | ||||
| // comparing "code id_token" and "id_token code" would evaluate to true.
 | ||||
| func ResponseTypesEqual(r1, r2 string) bool { | ||||
| 	if !strings.Contains(r1, " ") || !strings.Contains(r2, " ") { | ||||
| 		// fast route, no split needed
 | ||||
| 		return r1 == r2 | ||||
| 	} | ||||
| 
 | ||||
| 	// split, sort, and compare
 | ||||
| 	r1Fields := strings.Fields(r1) | ||||
| 	r2Fields := strings.Fields(r2) | ||||
| 	if len(r1Fields) != len(r2Fields) { | ||||
| 		return false | ||||
| 	} | ||||
| 	sort.Strings(r1Fields) | ||||
| 	sort.Strings(r2Fields) | ||||
| 	for i, r1Field := range r1Fields { | ||||
| 		if r1Field != r2Fields[i] { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	// OAuth2.0 response types registered by OIDC.
 | ||||
| 	//
 | ||||
| 	// See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#RegistryContents
 | ||||
| 	ResponseTypeCode             = "code" | ||||
| 	ResponseTypeCodeIDToken      = "code id_token" | ||||
| 	ResponseTypeCodeIDTokenToken = "code id_token token" | ||||
| 	ResponseTypeIDToken          = "id_token" | ||||
| 	ResponseTypeIDTokenToken     = "id_token token" | ||||
| 	ResponseTypeToken            = "token" | ||||
| 	ResponseTypeNone             = "none" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	GrantTypeAuthCode     = "authorization_code" | ||||
| 	GrantTypeClientCreds  = "client_credentials" | ||||
| 	GrantTypeUserCreds    = "password" | ||||
| 	GrantTypeImplicit     = "implicit" | ||||
| 	GrantTypeRefreshToken = "refresh_token" | ||||
| 
 | ||||
| 	AuthMethodClientSecretPost  = "client_secret_post" | ||||
| 	AuthMethodClientSecretBasic = "client_secret_basic" | ||||
| 	AuthMethodClientSecretJWT   = "client_secret_jwt" | ||||
| 	AuthMethodPrivateKeyJWT     = "private_key_jwt" | ||||
| ) | ||||
| 
 | ||||
| type Config struct { | ||||
| 	Credentials ClientCredentials | ||||
| 	Scope       []string | ||||
| 	RedirectURL string | ||||
| 	AuthURL     string | ||||
| 	TokenURL    string | ||||
| 
 | ||||
| 	// Must be one of the AuthMethodXXX methods above. Right now, only
 | ||||
| 	// AuthMethodClientSecretPost and AuthMethodClientSecretBasic are supported.
 | ||||
| 	AuthMethod string | ||||
| } | ||||
| 
 | ||||
| type Client struct { | ||||
| 	hc          phttp.Client | ||||
| 	creds       ClientCredentials | ||||
| 	scope       []string | ||||
| 	authURL     *url.URL | ||||
| 	redirectURL *url.URL | ||||
| 	tokenURL    *url.URL | ||||
| 	authMethod  string | ||||
| } | ||||
| 
 | ||||
| type ClientCredentials struct { | ||||
| 	ID     string | ||||
| 	Secret string | ||||
| } | ||||
| 
 | ||||
| func NewClient(hc phttp.Client, cfg Config) (c *Client, err error) { | ||||
| 	if len(cfg.Credentials.ID) == 0 { | ||||
| 		err = errors.New("missing client id") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if len(cfg.Credentials.Secret) == 0 { | ||||
| 		err = errors.New("missing client secret") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if cfg.AuthMethod == "" { | ||||
| 		cfg.AuthMethod = AuthMethodClientSecretBasic | ||||
| 	} else if cfg.AuthMethod != AuthMethodClientSecretPost && cfg.AuthMethod != AuthMethodClientSecretBasic { | ||||
| 		err = fmt.Errorf("auth method %q is not supported", cfg.AuthMethod) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	au, err := phttp.ParseNonEmptyURL(cfg.AuthURL) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	tu, err := phttp.ParseNonEmptyURL(cfg.TokenURL) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Allow empty redirect URL in the case where the client
 | ||||
| 	// only needs to verify a given token.
 | ||||
| 	ru, err := url.Parse(cfg.RedirectURL) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c = &Client{ | ||||
| 		creds:       cfg.Credentials, | ||||
| 		scope:       cfg.Scope, | ||||
| 		redirectURL: ru, | ||||
| 		authURL:     au, | ||||
| 		tokenURL:    tu, | ||||
| 		hc:          hc, | ||||
| 		authMethod:  cfg.AuthMethod, | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // Return the embedded HTTP client
 | ||||
| func (c *Client) HttpClient() phttp.Client { | ||||
| 	return c.hc | ||||
| } | ||||
| 
 | ||||
| // Generate the url for initial redirect to oauth provider.
 | ||||
| func (c *Client) AuthCodeURL(state, accessType, prompt string) string { | ||||
| 	v := c.commonURLValues() | ||||
| 	v.Set("state", state) | ||||
| 	if strings.ToLower(accessType) == "offline" { | ||||
| 		v.Set("access_type", "offline") | ||||
| 	} | ||||
| 
 | ||||
| 	if prompt != "" { | ||||
| 		v.Set("prompt", prompt) | ||||
| 	} | ||||
| 	v.Set("response_type", "code") | ||||
| 
 | ||||
| 	q := v.Encode() | ||||
| 	u := *c.authURL | ||||
| 	if u.RawQuery == "" { | ||||
| 		u.RawQuery = q | ||||
| 	} else { | ||||
| 		u.RawQuery += "&" + q | ||||
| 	} | ||||
| 	return u.String() | ||||
| } | ||||
| 
 | ||||
| func (c *Client) commonURLValues() url.Values { | ||||
| 	return url.Values{ | ||||
| 		"redirect_uri": {c.redirectURL.String()}, | ||||
| 		"scope":        {strings.Join(c.scope, " ")}, | ||||
| 		"client_id":    {c.creds.ID}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *Client) newAuthenticatedRequest(urlToken string, values url.Values) (*http.Request, error) { | ||||
| 	var req *http.Request | ||||
| 	var err error | ||||
| 	switch c.authMethod { | ||||
| 	case AuthMethodClientSecretPost: | ||||
| 		values.Set("client_secret", c.creds.Secret) | ||||
| 		req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode())) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case AuthMethodClientSecretBasic: | ||||
| 		req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode())) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		encodedID := url.QueryEscape(c.creds.ID) | ||||
| 		encodedSecret := url.QueryEscape(c.creds.Secret) | ||||
| 		req.SetBasicAuth(encodedID, encodedSecret) | ||||
| 	default: | ||||
| 		panic("misconfigured client: auth method not supported") | ||||
| 	} | ||||
| 
 | ||||
| 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||||
| 	return req, nil | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // ClientCredsToken posts the client id and secret to obtain a token scoped to the OAuth2 client via the "client_credentials" grant type.
 | ||||
| // May not be supported by all OAuth2 servers.
 | ||||
| func (c *Client) ClientCredsToken(scope []string) (result TokenResponse, err error) { | ||||
| 	v := url.Values{ | ||||
| 		"scope":      {strings.Join(scope, " ")}, | ||||
| 		"grant_type": {GrantTypeClientCreds}, | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := c.hc.Do(req) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	return parseTokenResponse(resp) | ||||
| } | ||||
| 
 | ||||
| // UserCredsToken posts the username and password to obtain a token scoped to the OAuth2 client via the "password" grant_type
 | ||||
| // May not be supported by all OAuth2 servers.
 | ||||
| func (c *Client) UserCredsToken(username, password string) (result TokenResponse, err error) { | ||||
| 	v := url.Values{ | ||||
| 		"scope":      {strings.Join(c.scope, " ")}, | ||||
| 		"grant_type": {GrantTypeUserCreds}, | ||||
| 		"username":   {username}, | ||||
| 		"password":   {password}, | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := c.hc.Do(req) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	return parseTokenResponse(resp) | ||||
| } | ||||
| 
 | ||||
| // RequestToken requests a token from the Token Endpoint with the specified grantType.
 | ||||
| // If 'grantType' == GrantTypeAuthCode, then 'value' should be the authorization code.
 | ||||
| // If 'grantType' == GrantTypeRefreshToken, then 'value' should be the refresh token.
 | ||||
| func (c *Client) RequestToken(grantType, value string) (result TokenResponse, err error) { | ||||
| 	v := c.commonURLValues() | ||||
| 
 | ||||
| 	v.Set("grant_type", grantType) | ||||
| 	v.Set("client_secret", c.creds.Secret) | ||||
| 	switch grantType { | ||||
| 	case GrantTypeAuthCode: | ||||
| 		v.Set("code", value) | ||||
| 	case GrantTypeRefreshToken: | ||||
| 		v.Set("refresh_token", value) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("unsupported grant_type: %v", grantType) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := c.hc.Do(req) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	return parseTokenResponse(resp) | ||||
| } | ||||
| 
 | ||||
| func parseTokenResponse(resp *http.Response) (result TokenResponse, err error) { | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	badStatusCode := resp.StatusCode < 200 || resp.StatusCode > 299 | ||||
| 
 | ||||
| 	contentType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	result = TokenResponse{ | ||||
| 		RawBody: body, | ||||
| 	} | ||||
| 
 | ||||
| 	newError := func(typ, desc, state string) error { | ||||
| 		if typ == "" { | ||||
| 			return fmt.Errorf("unrecognized error %s", body) | ||||
| 		} | ||||
| 		return &Error{typ, desc, state} | ||||
| 	} | ||||
| 
 | ||||
| 	if contentType == "application/x-www-form-urlencoded" || contentType == "text/plain" { | ||||
| 		var vals url.Values | ||||
| 		vals, err = url.ParseQuery(string(body)) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if error := vals.Get("error"); error != "" || badStatusCode { | ||||
| 			err = newError(error, vals.Get("error_description"), vals.Get("state")) | ||||
| 			return | ||||
| 		} | ||||
| 		e := vals.Get("expires_in") | ||||
| 		if e == "" { | ||||
| 			e = vals.Get("expires") | ||||
| 		} | ||||
| 		if e != "" { | ||||
| 			result.Expires, err = strconv.Atoi(e) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 		result.AccessToken = vals.Get("access_token") | ||||
| 		result.TokenType = vals.Get("token_type") | ||||
| 		result.IDToken = vals.Get("id_token") | ||||
| 		result.RefreshToken = vals.Get("refresh_token") | ||||
| 		result.Scope = vals.Get("scope") | ||||
| 	} else { | ||||
| 		var r struct { | ||||
| 			AccessToken  string      `json:"access_token"` | ||||
| 			TokenType    string      `json:"token_type"` | ||||
| 			IDToken      string      `json:"id_token"` | ||||
| 			RefreshToken string      `json:"refresh_token"` | ||||
| 			Scope        string      `json:"scope"` | ||||
| 			State        string      `json:"state"` | ||||
| 			ExpiresIn    json.Number `json:"expires_in"` // Azure AD returns string
 | ||||
| 			Expires      int         `json:"expires"` | ||||
| 			Error        string      `json:"error"` | ||||
| 			Desc         string      `json:"error_description"` | ||||
| 		} | ||||
| 		if err = json.Unmarshal(body, &r); err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if r.Error != "" || badStatusCode { | ||||
| 			err = newError(r.Error, r.Desc, r.State) | ||||
| 			return | ||||
| 		} | ||||
| 		result.AccessToken = r.AccessToken | ||||
| 		result.TokenType = r.TokenType | ||||
| 		result.IDToken = r.IDToken | ||||
| 		result.RefreshToken = r.RefreshToken | ||||
| 		result.Scope = r.Scope | ||||
| 		if expiresIn, err := r.ExpiresIn.Int64(); err != nil { | ||||
| 			result.Expires = r.Expires | ||||
| 		} else { | ||||
| 			result.Expires = int(expiresIn) | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| type TokenResponse struct { | ||||
| 	AccessToken  string | ||||
| 	TokenType    string | ||||
| 	Expires      int | ||||
| 	IDToken      string | ||||
| 	RefreshToken string // OPTIONAL.
 | ||||
| 	Scope        string // OPTIONAL, if identical to the scope requested by the client, otherwise, REQUIRED.
 | ||||
| 	RawBody      []byte // In case callers need some other non-standard info from the token response
 | ||||
| } | ||||
| 
 | ||||
| type AuthCodeRequest struct { | ||||
| 	ResponseType string | ||||
| 	ClientID     string | ||||
| 	RedirectURL  *url.URL | ||||
| 	Scope        []string | ||||
| 	State        string | ||||
| } | ||||
| 
 | ||||
| func ParseAuthCodeRequest(q url.Values) (AuthCodeRequest, error) { | ||||
| 	acr := AuthCodeRequest{ | ||||
| 		ResponseType: q.Get("response_type"), | ||||
| 		ClientID:     q.Get("client_id"), | ||||
| 		State:        q.Get("state"), | ||||
| 		Scope:        make([]string, 0), | ||||
| 	} | ||||
| 
 | ||||
| 	qs := strings.TrimSpace(q.Get("scope")) | ||||
| 	if qs != "" { | ||||
| 		acr.Scope = strings.Split(qs, " ") | ||||
| 	} | ||||
| 
 | ||||
| 	err := func() error { | ||||
| 		if acr.ClientID == "" { | ||||
| 			return NewError(ErrorInvalidRequest) | ||||
| 		} | ||||
| 
 | ||||
| 		redirectURL := q.Get("redirect_uri") | ||||
| 		if redirectURL != "" { | ||||
| 			ru, err := url.Parse(redirectURL) | ||||
| 			if err != nil { | ||||
| 				return NewError(ErrorInvalidRequest) | ||||
| 			} | ||||
| 			acr.RedirectURL = ru | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}() | ||||
| 
 | ||||
| 	return acr, err | ||||
| } | ||||
|  | @ -0,0 +1,374 @@ | |||
| // Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
 | ||||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/sha256" | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"hash" | ||||
| 	"io/ioutil" | ||||
| 	"mime" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"golang.org/x/oauth2" | ||||
| 	jose "gopkg.in/square/go-jose.v2" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests.
 | ||||
| 	ScopeOpenID = "openid" | ||||
| 
 | ||||
| 	// ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting
 | ||||
| 	// OAuth2 refresh tokens.
 | ||||
| 	//
 | ||||
| 	// Support for this scope differs between OpenID Connect providers. For instance
 | ||||
| 	// Google rejects it, favoring appending "access_type=offline" as part of the
 | ||||
| 	// authorization request instead.
 | ||||
| 	//
 | ||||
| 	// See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
 | ||||
| 	ScopeOfflineAccess = "offline_access" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	errNoAtHash      = errors.New("id token did not have an access token hash") | ||||
| 	errInvalidAtHash = errors.New("access token hash does not match value in ID token") | ||||
| ) | ||||
| 
 | ||||
| // ClientContext returns a new Context that carries the provided HTTP client.
 | ||||
| //
 | ||||
| // This method sets the same context key used by the golang.org/x/oauth2 package,
 | ||||
| // so the returned context works for that package too.
 | ||||
| //
 | ||||
| //    myClient := &http.Client{}
 | ||||
| //    ctx := oidc.ClientContext(parentContext, myClient)
 | ||||
| //
 | ||||
| //    // This will use the custom client
 | ||||
| //    provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
 | ||||
| //
 | ||||
| func ClientContext(ctx context.Context, client *http.Client) context.Context { | ||||
| 	return context.WithValue(ctx, oauth2.HTTPClient, client) | ||||
| } | ||||
| 
 | ||||
| func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { | ||||
| 	client := http.DefaultClient | ||||
| 	if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { | ||||
| 		client = c | ||||
| 	} | ||||
| 	return client.Do(req.WithContext(ctx)) | ||||
| } | ||||
| 
 | ||||
| // Provider represents an OpenID Connect server's configuration.
 | ||||
| type Provider struct { | ||||
| 	issuer      string | ||||
| 	authURL     string | ||||
| 	tokenURL    string | ||||
| 	userInfoURL string | ||||
| 
 | ||||
| 	// Raw claims returned by the server.
 | ||||
| 	rawClaims []byte | ||||
| 
 | ||||
| 	remoteKeySet KeySet | ||||
| } | ||||
| 
 | ||||
| type cachedKeys struct { | ||||
| 	keys   []jose.JSONWebKey | ||||
| 	expiry time.Time | ||||
| } | ||||
| 
 | ||||
| type providerJSON struct { | ||||
| 	Issuer      string `json:"issuer"` | ||||
| 	AuthURL     string `json:"authorization_endpoint"` | ||||
| 	TokenURL    string `json:"token_endpoint"` | ||||
| 	JWKSURL     string `json:"jwks_uri"` | ||||
| 	UserInfoURL string `json:"userinfo_endpoint"` | ||||
| } | ||||
| 
 | ||||
| // NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
 | ||||
| //
 | ||||
| // The issuer is the URL identifier for the service. For example: "https://accounts.google.com"
 | ||||
| // or "https://login.salesforce.com".
 | ||||
| func NewProvider(ctx context.Context, issuer string) (*Provider, error) { | ||||
| 	wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" | ||||
| 	req, err := http.NewRequest("GET", wellKnown, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	resp, err := doRequest(ctx, req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unable to read response body: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
| 
 | ||||
| 	var p providerJSON | ||||
| 	err = unmarshalResp(resp, body, &p) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if p.Issuer != issuer { | ||||
| 		return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer) | ||||
| 	} | ||||
| 	return &Provider{ | ||||
| 		issuer:       p.Issuer, | ||||
| 		authURL:      p.AuthURL, | ||||
| 		tokenURL:     p.TokenURL, | ||||
| 		userInfoURL:  p.UserInfoURL, | ||||
| 		rawClaims:    body, | ||||
| 		remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Claims unmarshals raw fields returned by the server during discovery.
 | ||||
| //
 | ||||
| //    var claims struct {
 | ||||
| //        ScopesSupported []string `json:"scopes_supported"`
 | ||||
| //        ClaimsSupported []string `json:"claims_supported"`
 | ||||
| //    }
 | ||||
| //
 | ||||
| //    if err := provider.Claims(&claims); err != nil {
 | ||||
| //        // handle unmarshaling error
 | ||||
| //    }
 | ||||
| //
 | ||||
| // For a list of fields defined by the OpenID Connect spec see:
 | ||||
| // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
 | ||||
| func (p *Provider) Claims(v interface{}) error { | ||||
| 	if p.rawClaims == nil { | ||||
| 		return errors.New("oidc: claims not set") | ||||
| 	} | ||||
| 	return json.Unmarshal(p.rawClaims, v) | ||||
| } | ||||
| 
 | ||||
| // Endpoint returns the OAuth2 auth and token endpoints for the given provider.
 | ||||
| func (p *Provider) Endpoint() oauth2.Endpoint { | ||||
| 	return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL} | ||||
| } | ||||
| 
 | ||||
| // UserInfo represents the OpenID Connect userinfo claims.
 | ||||
| type UserInfo struct { | ||||
| 	Subject       string `json:"sub"` | ||||
| 	Profile       string `json:"profile"` | ||||
| 	Email         string `json:"email"` | ||||
| 	EmailVerified bool   `json:"email_verified"` | ||||
| 
 | ||||
| 	claims []byte | ||||
| } | ||||
| 
 | ||||
| // Claims unmarshals the raw JSON object claims into the provided object.
 | ||||
| func (u *UserInfo) Claims(v interface{}) error { | ||||
| 	if u.claims == nil { | ||||
| 		return errors.New("oidc: claims not set") | ||||
| 	} | ||||
| 	return json.Unmarshal(u.claims, v) | ||||
| } | ||||
| 
 | ||||
| // UserInfo uses the token source to query the provider's user info endpoint.
 | ||||
| func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) { | ||||
| 	if p.userInfoURL == "" { | ||||
| 		return nil, errors.New("oidc: user info endpoint is not supported by this provider") | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", p.userInfoURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: create GET request: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	token, err := tokenSource.Token() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: get access token: %v", err) | ||||
| 	} | ||||
| 	token.SetAuthHeader(req) | ||||
| 
 | ||||
| 	resp, err := doRequest(ctx, req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("%s: %s", resp.Status, body) | ||||
| 	} | ||||
| 
 | ||||
| 	var userInfo UserInfo | ||||
| 	if err := json.Unmarshal(body, &userInfo); err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err) | ||||
| 	} | ||||
| 	userInfo.claims = body | ||||
| 	return &userInfo, nil | ||||
| } | ||||
| 
 | ||||
| // IDToken is an OpenID Connect extension that provides a predictable representation
 | ||||
| // of an authorization event.
 | ||||
| //
 | ||||
| // The ID Token only holds fields OpenID Connect requires. To access additional
 | ||||
| // claims returned by the server, use the Claims method.
 | ||||
| type IDToken struct { | ||||
| 	// The URL of the server which issued this token. OpenID Connect
 | ||||
| 	// requires this value always be identical to the URL used for
 | ||||
| 	// initial discovery.
 | ||||
| 	//
 | ||||
| 	// Note: Because of a known issue with Google Accounts' implementation
 | ||||
| 	// this value may differ when using Google.
 | ||||
| 	//
 | ||||
| 	// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
 | ||||
| 	Issuer string | ||||
| 
 | ||||
| 	// The client ID, or set of client IDs, that this token is issued for. For
 | ||||
| 	// common uses, this is the client that initialized the auth flow.
 | ||||
| 	//
 | ||||
| 	// This package ensures the audience contains an expected value.
 | ||||
| 	Audience []string | ||||
| 
 | ||||
| 	// A unique string which identifies the end user.
 | ||||
| 	Subject string | ||||
| 
 | ||||
| 	// Expiry of the token. Ths package will not process tokens that have
 | ||||
| 	// expired unless that validation is explicitly turned off.
 | ||||
| 	Expiry time.Time | ||||
| 	// When the token was issued by the provider.
 | ||||
| 	IssuedAt time.Time | ||||
| 
 | ||||
| 	// Initial nonce provided during the authentication redirect.
 | ||||
| 	//
 | ||||
| 	// This package does NOT provided verification on the value of this field
 | ||||
| 	// and it's the user's responsibility to ensure it contains a valid value.
 | ||||
| 	Nonce string | ||||
| 
 | ||||
| 	// at_hash claim, if set in the ID token. Callers can verify an access token
 | ||||
| 	// that corresponds to the ID token using the VerifyAccessToken method.
 | ||||
| 	AccessTokenHash string | ||||
| 
 | ||||
| 	// signature algorithm used for ID token, needed to compute a verification hash of an
 | ||||
| 	// access token
 | ||||
| 	sigAlgorithm string | ||||
| 
 | ||||
| 	// Raw payload of the id_token.
 | ||||
| 	claims []byte | ||||
| } | ||||
| 
 | ||||
| // Claims unmarshals the raw JSON payload of the ID Token into a provided struct.
 | ||||
| //
 | ||||
| //		idToken, err := idTokenVerifier.Verify(rawIDToken)
 | ||||
| //		if err != nil {
 | ||||
| //			// handle error
 | ||||
| //		}
 | ||||
| //		var claims struct {
 | ||||
| //			Email         string `json:"email"`
 | ||||
| //			EmailVerified bool   `json:"email_verified"`
 | ||||
| //		}
 | ||||
| //		if err := idToken.Claims(&claims); err != nil {
 | ||||
| //			// handle error
 | ||||
| //		}
 | ||||
| //
 | ||||
| func (i *IDToken) Claims(v interface{}) error { | ||||
| 	if i.claims == nil { | ||||
| 		return errors.New("oidc: claims not set") | ||||
| 	} | ||||
| 	return json.Unmarshal(i.claims, v) | ||||
| } | ||||
| 
 | ||||
| // VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token
 | ||||
| // matches the hash in the id token. It returns an error if the hashes  don't match.
 | ||||
| // It is the caller's responsibility to ensure that the optional access token hash is present for the ID token
 | ||||
| // before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
 | ||||
| func (i *IDToken) VerifyAccessToken(accessToken string) error { | ||||
| 	if i.AccessTokenHash == "" { | ||||
| 		return errNoAtHash | ||||
| 	} | ||||
| 	var h hash.Hash | ||||
| 	switch i.sigAlgorithm { | ||||
| 	case RS256, ES256, PS256: | ||||
| 		h = sha256.New() | ||||
| 	case RS384, ES384, PS384: | ||||
| 		h = sha512.New384() | ||||
| 	case RS512, ES512, PS512: | ||||
| 		h = sha512.New() | ||||
| 	default: | ||||
| 		return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm) | ||||
| 	} | ||||
| 	h.Write([]byte(accessToken)) // hash documents that Write will never return an error
 | ||||
| 	sum := h.Sum(nil)[:h.Size()/2] | ||||
| 	actual := base64.RawURLEncoding.EncodeToString(sum) | ||||
| 	if actual != i.AccessTokenHash { | ||||
| 		return errInvalidAtHash | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type idToken struct { | ||||
| 	Issuer   string   `json:"iss"` | ||||
| 	Subject  string   `json:"sub"` | ||||
| 	Audience audience `json:"aud"` | ||||
| 	Expiry   jsonTime `json:"exp"` | ||||
| 	IssuedAt jsonTime `json:"iat"` | ||||
| 	Nonce    string   `json:"nonce"` | ||||
| 	AtHash   string   `json:"at_hash"` | ||||
| } | ||||
| 
 | ||||
| type audience []string | ||||
| 
 | ||||
| func (a *audience) UnmarshalJSON(b []byte) error { | ||||
| 	var s string | ||||
| 	if json.Unmarshal(b, &s) == nil { | ||||
| 		*a = audience{s} | ||||
| 		return nil | ||||
| 	} | ||||
| 	var auds []string | ||||
| 	if err := json.Unmarshal(b, &auds); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*a = audience(auds) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type jsonTime time.Time | ||||
| 
 | ||||
| func (j *jsonTime) UnmarshalJSON(b []byte) error { | ||||
| 	var n json.Number | ||||
| 	if err := json.Unmarshal(b, &n); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	var unix int64 | ||||
| 
 | ||||
| 	if t, err := n.Int64(); err == nil { | ||||
| 		unix = t | ||||
| 	} else { | ||||
| 		f, err := n.Float64() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		unix = int64(f) | ||||
| 	} | ||||
| 	*j = jsonTime(time.Unix(unix, 0)) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func unmarshalResp(r *http.Response, body []byte, v interface{}) error { | ||||
| 	err := json.Unmarshal(body, &v) | ||||
| 	if err == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	ct := r.Header.Get("Content-Type") | ||||
| 	mediaType, _, parseErr := mime.ParseMediaType(ct) | ||||
| 	if parseErr == nil && mediaType == "application/json" { | ||||
| 		return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err) | ||||
| 	} | ||||
| 	return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err) | ||||
| } | ||||
|  | @ -1,846 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/mail" | ||||
| 	"net/url" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	phttp "github.com/coreos/go-oidc/http" | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| 	"github.com/coreos/go-oidc/key" | ||||
| 	"github.com/coreos/go-oidc/oauth2" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// amount of time that must pass after the last key sync
 | ||||
| 	// completes before another attempt may begin
 | ||||
| 	keySyncWindow = 5 * time.Second | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	DefaultScope = []string{"openid", "email", "profile"} | ||||
| 
 | ||||
| 	supportedAuthMethods = map[string]struct{}{ | ||||
| 		oauth2.AuthMethodClientSecretBasic: struct{}{}, | ||||
| 		oauth2.AuthMethodClientSecretPost:  struct{}{}, | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| type ClientCredentials oauth2.ClientCredentials | ||||
| 
 | ||||
| type ClientIdentity struct { | ||||
| 	Credentials ClientCredentials | ||||
| 	Metadata    ClientMetadata | ||||
| } | ||||
| 
 | ||||
| type JWAOptions struct { | ||||
| 	// SigningAlg specifies an JWA alg for signing JWTs.
 | ||||
| 	//
 | ||||
| 	// Specifying this field implies different actions depending on the context. It may
 | ||||
| 	// require objects be serialized and signed as a JWT instead of plain JSON, or
 | ||||
| 	// require an existing JWT object use the specified alg.
 | ||||
| 	//
 | ||||
| 	// See: http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
 | ||||
| 	SigningAlg string | ||||
| 	// EncryptionAlg, if provided, specifies that the returned or sent object be stored
 | ||||
| 	// (or nested) within a JWT object and encrypted with the provided JWA alg.
 | ||||
| 	EncryptionAlg string | ||||
| 	// EncryptionEnc specifies the JWA enc algorithm to use with EncryptionAlg. If
 | ||||
| 	// EncryptionAlg is provided and EncryptionEnc is omitted, this field defaults
 | ||||
| 	// to A128CBC-HS256.
 | ||||
| 	//
 | ||||
| 	// If EncryptionEnc is provided EncryptionAlg must also be specified.
 | ||||
| 	EncryptionEnc string | ||||
| } | ||||
| 
 | ||||
| func (opt JWAOptions) valid() error { | ||||
| 	if opt.EncryptionEnc != "" && opt.EncryptionAlg == "" { | ||||
| 		return errors.New("encryption encoding provided with no encryption algorithm") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (opt JWAOptions) defaults() JWAOptions { | ||||
| 	if opt.EncryptionAlg != "" && opt.EncryptionEnc == "" { | ||||
| 		opt.EncryptionEnc = jose.EncA128CBCHS256 | ||||
| 	} | ||||
| 	return opt | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	// Ensure ClientMetadata satisfies these interfaces.
 | ||||
| 	_ json.Marshaler   = &ClientMetadata{} | ||||
| 	_ json.Unmarshaler = &ClientMetadata{} | ||||
| ) | ||||
| 
 | ||||
| // ClientMetadata holds metadata that the authorization server associates
 | ||||
| // with a client identifier. The fields range from human-facing display
 | ||||
| // strings such as client name, to items that impact the security of the
 | ||||
| // protocol, such as the list of valid redirect URIs.
 | ||||
| //
 | ||||
| // See http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
 | ||||
| //
 | ||||
| // TODO: support language specific claim representations
 | ||||
| // http://openid.net/specs/openid-connect-registration-1_0.html#LanguagesAndScripts
 | ||||
| type ClientMetadata struct { | ||||
| 	RedirectURIs []url.URL // Required
 | ||||
| 
 | ||||
| 	// A list of OAuth 2.0 "response_type" values that the client wishes to restrict
 | ||||
| 	// itself to. Either "code", "token", or another registered extension.
 | ||||
| 	//
 | ||||
| 	// If omitted, only "code" will be used.
 | ||||
| 	ResponseTypes []string | ||||
| 	// A list of OAuth 2.0 grant types the client wishes to restrict itself to.
 | ||||
| 	// The grant type values used by OIDC are "authorization_code", "implicit",
 | ||||
| 	// and "refresh_token".
 | ||||
| 	//
 | ||||
| 	// If ommitted, only "authorization_code" will be used.
 | ||||
| 	GrantTypes []string | ||||
| 	// "native" or "web". If omitted, "web".
 | ||||
| 	ApplicationType string | ||||
| 
 | ||||
| 	// List of email addresses.
 | ||||
| 	Contacts []mail.Address | ||||
| 	// Name of client to be presented to the end-user.
 | ||||
| 	ClientName string | ||||
| 	// URL that references a logo for the Client application.
 | ||||
| 	LogoURI *url.URL | ||||
| 	// URL of the home page of the Client.
 | ||||
| 	ClientURI *url.URL | ||||
| 	// Profile data policies and terms of use to be provided to the end user.
 | ||||
| 	PolicyURI         *url.URL | ||||
| 	TermsOfServiceURI *url.URL | ||||
| 
 | ||||
| 	// URL to or the value of the client's JSON Web Key Set document.
 | ||||
| 	JWKSURI *url.URL | ||||
| 	JWKS    *jose.JWKSet | ||||
| 
 | ||||
| 	// URL referencing a flie with a single JSON array of redirect URIs.
 | ||||
| 	SectorIdentifierURI *url.URL | ||||
| 
 | ||||
| 	SubjectType string | ||||
| 
 | ||||
| 	// Options to restrict the JWS alg and enc values used for server responses and requests.
 | ||||
| 	IDTokenResponseOptions  JWAOptions | ||||
| 	UserInfoResponseOptions JWAOptions | ||||
| 	RequestObjectOptions    JWAOptions | ||||
| 
 | ||||
| 	// Client requested authorization method and signing options for the token endpoint.
 | ||||
| 	//
 | ||||
| 	// Defaults to "client_secret_basic"
 | ||||
| 	TokenEndpointAuthMethod     string | ||||
| 	TokenEndpointAuthSigningAlg string | ||||
| 
 | ||||
| 	// DefaultMaxAge specifies the maximum amount of time in seconds before an authorized
 | ||||
| 	// user must reauthroize.
 | ||||
| 	//
 | ||||
| 	// If 0, no limitation is placed on the maximum.
 | ||||
| 	DefaultMaxAge int64 | ||||
| 	// RequireAuthTime specifies if the auth_time claim in the ID token is required.
 | ||||
| 	RequireAuthTime bool | ||||
| 
 | ||||
| 	// Default Authentication Context Class Reference values for authentication requests.
 | ||||
| 	DefaultACRValues []string | ||||
| 
 | ||||
| 	// URI that a third party can use to initiate a login by the relaying party.
 | ||||
| 	//
 | ||||
| 	// See: http://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
 | ||||
| 	InitiateLoginURI *url.URL | ||||
| 	// Pre-registered request_uri values that may be cached by the server.
 | ||||
| 	RequestURIs []url.URL | ||||
| } | ||||
| 
 | ||||
| // Defaults returns a shallow copy of ClientMetadata with default
 | ||||
| // values replacing omitted fields.
 | ||||
| func (m ClientMetadata) Defaults() ClientMetadata { | ||||
| 	if len(m.ResponseTypes) == 0 { | ||||
| 		m.ResponseTypes = []string{oauth2.ResponseTypeCode} | ||||
| 	} | ||||
| 	if len(m.GrantTypes) == 0 { | ||||
| 		m.GrantTypes = []string{oauth2.GrantTypeAuthCode} | ||||
| 	} | ||||
| 	if m.ApplicationType == "" { | ||||
| 		m.ApplicationType = "web" | ||||
| 	} | ||||
| 	if m.TokenEndpointAuthMethod == "" { | ||||
| 		m.TokenEndpointAuthMethod = oauth2.AuthMethodClientSecretBasic | ||||
| 	} | ||||
| 	m.IDTokenResponseOptions = m.IDTokenResponseOptions.defaults() | ||||
| 	m.UserInfoResponseOptions = m.UserInfoResponseOptions.defaults() | ||||
| 	m.RequestObjectOptions = m.RequestObjectOptions.defaults() | ||||
| 	return m | ||||
| } | ||||
| 
 | ||||
| func (m *ClientMetadata) MarshalJSON() ([]byte, error) { | ||||
| 	e := m.toEncodableStruct() | ||||
| 	return json.Marshal(&e) | ||||
| } | ||||
| 
 | ||||
| func (m *ClientMetadata) UnmarshalJSON(data []byte) error { | ||||
| 	var e encodableClientMetadata | ||||
| 	if err := json.Unmarshal(data, &e); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	meta, err := e.toStruct() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := meta.Valid(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*m = meta | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type encodableClientMetadata struct { | ||||
| 	RedirectURIs                 []string     `json:"redirect_uris"` // Required
 | ||||
| 	ResponseTypes                []string     `json:"response_types,omitempty"` | ||||
| 	GrantTypes                   []string     `json:"grant_types,omitempty"` | ||||
| 	ApplicationType              string       `json:"application_type,omitempty"` | ||||
| 	Contacts                     []string     `json:"contacts,omitempty"` | ||||
| 	ClientName                   string       `json:"client_name,omitempty"` | ||||
| 	LogoURI                      string       `json:"logo_uri,omitempty"` | ||||
| 	ClientURI                    string       `json:"client_uri,omitempty"` | ||||
| 	PolicyURI                    string       `json:"policy_uri,omitempty"` | ||||
| 	TermsOfServiceURI            string       `json:"tos_uri,omitempty"` | ||||
| 	JWKSURI                      string       `json:"jwks_uri,omitempty"` | ||||
| 	JWKS                         *jose.JWKSet `json:"jwks,omitempty"` | ||||
| 	SectorIdentifierURI          string       `json:"sector_identifier_uri,omitempty"` | ||||
| 	SubjectType                  string       `json:"subject_type,omitempty"` | ||||
| 	IDTokenSignedResponseAlg     string       `json:"id_token_signed_response_alg,omitempty"` | ||||
| 	IDTokenEncryptedResponseAlg  string       `json:"id_token_encrypted_response_alg,omitempty"` | ||||
| 	IDTokenEncryptedResponseEnc  string       `json:"id_token_encrypted_response_enc,omitempty"` | ||||
| 	UserInfoSignedResponseAlg    string       `json:"userinfo_signed_response_alg,omitempty"` | ||||
| 	UserInfoEncryptedResponseAlg string       `json:"userinfo_encrypted_response_alg,omitempty"` | ||||
| 	UserInfoEncryptedResponseEnc string       `json:"userinfo_encrypted_response_enc,omitempty"` | ||||
| 	RequestObjectSigningAlg      string       `json:"request_object_signing_alg,omitempty"` | ||||
| 	RequestObjectEncryptionAlg   string       `json:"request_object_encryption_alg,omitempty"` | ||||
| 	RequestObjectEncryptionEnc   string       `json:"request_object_encryption_enc,omitempty"` | ||||
| 	TokenEndpointAuthMethod      string       `json:"token_endpoint_auth_method,omitempty"` | ||||
| 	TokenEndpointAuthSigningAlg  string       `json:"token_endpoint_auth_signing_alg,omitempty"` | ||||
| 	DefaultMaxAge                int64        `json:"default_max_age,omitempty"` | ||||
| 	RequireAuthTime              bool         `json:"require_auth_time,omitempty"` | ||||
| 	DefaultACRValues             []string     `json:"default_acr_values,omitempty"` | ||||
| 	InitiateLoginURI             string       `json:"initiate_login_uri,omitempty"` | ||||
| 	RequestURIs                  []string     `json:"request_uris,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func (c *encodableClientMetadata) toStruct() (ClientMetadata, error) { | ||||
| 	p := stickyErrParser{} | ||||
| 	m := ClientMetadata{ | ||||
| 		RedirectURIs:                p.parseURIs(c.RedirectURIs, "redirect_uris"), | ||||
| 		ResponseTypes:               c.ResponseTypes, | ||||
| 		GrantTypes:                  c.GrantTypes, | ||||
| 		ApplicationType:             c.ApplicationType, | ||||
| 		Contacts:                    p.parseEmails(c.Contacts, "contacts"), | ||||
| 		ClientName:                  c.ClientName, | ||||
| 		LogoURI:                     p.parseURI(c.LogoURI, "logo_uri"), | ||||
| 		ClientURI:                   p.parseURI(c.ClientURI, "client_uri"), | ||||
| 		PolicyURI:                   p.parseURI(c.PolicyURI, "policy_uri"), | ||||
| 		TermsOfServiceURI:           p.parseURI(c.TermsOfServiceURI, "tos_uri"), | ||||
| 		JWKSURI:                     p.parseURI(c.JWKSURI, "jwks_uri"), | ||||
| 		JWKS:                        c.JWKS, | ||||
| 		SectorIdentifierURI:         p.parseURI(c.SectorIdentifierURI, "sector_identifier_uri"), | ||||
| 		SubjectType:                 c.SubjectType, | ||||
| 		TokenEndpointAuthMethod:     c.TokenEndpointAuthMethod, | ||||
| 		TokenEndpointAuthSigningAlg: c.TokenEndpointAuthSigningAlg, | ||||
| 		DefaultMaxAge:               c.DefaultMaxAge, | ||||
| 		RequireAuthTime:             c.RequireAuthTime, | ||||
| 		DefaultACRValues:            c.DefaultACRValues, | ||||
| 		InitiateLoginURI:            p.parseURI(c.InitiateLoginURI, "initiate_login_uri"), | ||||
| 		RequestURIs:                 p.parseURIs(c.RequestURIs, "request_uris"), | ||||
| 		IDTokenResponseOptions: JWAOptions{ | ||||
| 			c.IDTokenSignedResponseAlg, | ||||
| 			c.IDTokenEncryptedResponseAlg, | ||||
| 			c.IDTokenEncryptedResponseEnc, | ||||
| 		}, | ||||
| 		UserInfoResponseOptions: JWAOptions{ | ||||
| 			c.UserInfoSignedResponseAlg, | ||||
| 			c.UserInfoEncryptedResponseAlg, | ||||
| 			c.UserInfoEncryptedResponseEnc, | ||||
| 		}, | ||||
| 		RequestObjectOptions: JWAOptions{ | ||||
| 			c.RequestObjectSigningAlg, | ||||
| 			c.RequestObjectEncryptionAlg, | ||||
| 			c.RequestObjectEncryptionEnc, | ||||
| 		}, | ||||
| 	} | ||||
| 	if p.firstErr != nil { | ||||
| 		return ClientMetadata{}, p.firstErr | ||||
| 	} | ||||
| 	return m, nil | ||||
| } | ||||
| 
 | ||||
| // stickyErrParser parses URIs and email addresses. Once it encounters
 | ||||
| // a parse error, subsequent calls become no-op.
 | ||||
| type stickyErrParser struct { | ||||
| 	firstErr error | ||||
| } | ||||
| 
 | ||||
| func (p *stickyErrParser) parseURI(s, field string) *url.URL { | ||||
| 	if p.firstErr != nil || s == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 	u, err := url.Parse(s) | ||||
| 	if err == nil { | ||||
| 		if u.Host == "" { | ||||
| 			err = errors.New("no host in URI") | ||||
| 		} else if u.Scheme != "http" && u.Scheme != "https" { | ||||
| 			err = errors.New("invalid URI scheme") | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		p.firstErr = fmt.Errorf("failed to parse %s: %v", field, err) | ||||
| 		return nil | ||||
| 	} | ||||
| 	return u | ||||
| } | ||||
| 
 | ||||
| func (p *stickyErrParser) parseURIs(s []string, field string) []url.URL { | ||||
| 	if p.firstErr != nil || len(s) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	uris := make([]url.URL, len(s)) | ||||
| 	for i, val := range s { | ||||
| 		if val == "" { | ||||
| 			p.firstErr = fmt.Errorf("invalid URI in field %s", field) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if u := p.parseURI(val, field); u != nil { | ||||
| 			uris[i] = *u | ||||
| 		} | ||||
| 	} | ||||
| 	return uris | ||||
| } | ||||
| 
 | ||||
| func (p *stickyErrParser) parseEmails(s []string, field string) []mail.Address { | ||||
| 	if p.firstErr != nil || len(s) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	addrs := make([]mail.Address, len(s)) | ||||
| 	for i, addr := range s { | ||||
| 		if addr == "" { | ||||
| 			p.firstErr = fmt.Errorf("invalid email in field %s", field) | ||||
| 			return nil | ||||
| 		} | ||||
| 		a, err := mail.ParseAddress(addr) | ||||
| 		if err != nil { | ||||
| 			p.firstErr = fmt.Errorf("invalid email in field %s: %v", field, err) | ||||
| 			return nil | ||||
| 		} | ||||
| 		addrs[i] = *a | ||||
| 	} | ||||
| 	return addrs | ||||
| } | ||||
| 
 | ||||
| func (m *ClientMetadata) toEncodableStruct() encodableClientMetadata { | ||||
| 	return encodableClientMetadata{ | ||||
| 		RedirectURIs:                 urisToStrings(m.RedirectURIs), | ||||
| 		ResponseTypes:                m.ResponseTypes, | ||||
| 		GrantTypes:                   m.GrantTypes, | ||||
| 		ApplicationType:              m.ApplicationType, | ||||
| 		Contacts:                     emailsToStrings(m.Contacts), | ||||
| 		ClientName:                   m.ClientName, | ||||
| 		LogoURI:                      uriToString(m.LogoURI), | ||||
| 		ClientURI:                    uriToString(m.ClientURI), | ||||
| 		PolicyURI:                    uriToString(m.PolicyURI), | ||||
| 		TermsOfServiceURI:            uriToString(m.TermsOfServiceURI), | ||||
| 		JWKSURI:                      uriToString(m.JWKSURI), | ||||
| 		JWKS:                         m.JWKS, | ||||
| 		SectorIdentifierURI:          uriToString(m.SectorIdentifierURI), | ||||
| 		SubjectType:                  m.SubjectType, | ||||
| 		IDTokenSignedResponseAlg:     m.IDTokenResponseOptions.SigningAlg, | ||||
| 		IDTokenEncryptedResponseAlg:  m.IDTokenResponseOptions.EncryptionAlg, | ||||
| 		IDTokenEncryptedResponseEnc:  m.IDTokenResponseOptions.EncryptionEnc, | ||||
| 		UserInfoSignedResponseAlg:    m.UserInfoResponseOptions.SigningAlg, | ||||
| 		UserInfoEncryptedResponseAlg: m.UserInfoResponseOptions.EncryptionAlg, | ||||
| 		UserInfoEncryptedResponseEnc: m.UserInfoResponseOptions.EncryptionEnc, | ||||
| 		RequestObjectSigningAlg:      m.RequestObjectOptions.SigningAlg, | ||||
| 		RequestObjectEncryptionAlg:   m.RequestObjectOptions.EncryptionAlg, | ||||
| 		RequestObjectEncryptionEnc:   m.RequestObjectOptions.EncryptionEnc, | ||||
| 		TokenEndpointAuthMethod:      m.TokenEndpointAuthMethod, | ||||
| 		TokenEndpointAuthSigningAlg:  m.TokenEndpointAuthSigningAlg, | ||||
| 		DefaultMaxAge:                m.DefaultMaxAge, | ||||
| 		RequireAuthTime:              m.RequireAuthTime, | ||||
| 		DefaultACRValues:             m.DefaultACRValues, | ||||
| 		InitiateLoginURI:             uriToString(m.InitiateLoginURI), | ||||
| 		RequestURIs:                  urisToStrings(m.RequestURIs), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func uriToString(u *url.URL) string { | ||||
| 	if u == nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return u.String() | ||||
| } | ||||
| 
 | ||||
| func urisToStrings(urls []url.URL) []string { | ||||
| 	if len(urls) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	sli := make([]string, len(urls)) | ||||
| 	for i, u := range urls { | ||||
| 		sli[i] = u.String() | ||||
| 	} | ||||
| 	return sli | ||||
| } | ||||
| 
 | ||||
| func emailsToStrings(addrs []mail.Address) []string { | ||||
| 	if len(addrs) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	sli := make([]string, len(addrs)) | ||||
| 	for i, addr := range addrs { | ||||
| 		sli[i] = addr.String() | ||||
| 	} | ||||
| 	return sli | ||||
| } | ||||
| 
 | ||||
| // Valid determines if a ClientMetadata conforms with the OIDC specification.
 | ||||
| //
 | ||||
| // Valid is called by UnmarshalJSON.
 | ||||
| //
 | ||||
| // NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
 | ||||
| // URLs fields where the OIDC spec requires it. This may change in future releases
 | ||||
| // of this package. See: https://github.com/coreos/go-oidc/issues/34
 | ||||
| func (m *ClientMetadata) Valid() error { | ||||
| 	if len(m.RedirectURIs) == 0 { | ||||
| 		return errors.New("zero redirect URLs") | ||||
| 	} | ||||
| 
 | ||||
| 	validURI := func(u *url.URL, fieldName string) error { | ||||
| 		if u.Host == "" { | ||||
| 			return fmt.Errorf("no host for uri field %s", fieldName) | ||||
| 		} | ||||
| 		if u.Scheme != "http" && u.Scheme != "https" { | ||||
| 			return fmt.Errorf("uri field %s scheme is not http or https", fieldName) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	uris := []struct { | ||||
| 		val  *url.URL | ||||
| 		name string | ||||
| 	}{ | ||||
| 		{m.LogoURI, "logo_uri"}, | ||||
| 		{m.ClientURI, "client_uri"}, | ||||
| 		{m.PolicyURI, "policy_uri"}, | ||||
| 		{m.TermsOfServiceURI, "tos_uri"}, | ||||
| 		{m.JWKSURI, "jwks_uri"}, | ||||
| 		{m.SectorIdentifierURI, "sector_identifier_uri"}, | ||||
| 		{m.InitiateLoginURI, "initiate_login_uri"}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, uri := range uris { | ||||
| 		if uri.val == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		if err := validURI(uri.val, uri.name); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	uriLists := []struct { | ||||
| 		vals []url.URL | ||||
| 		name string | ||||
| 	}{ | ||||
| 		{m.RedirectURIs, "redirect_uris"}, | ||||
| 		{m.RequestURIs, "request_uris"}, | ||||
| 	} | ||||
| 	for _, list := range uriLists { | ||||
| 		for _, uri := range list.vals { | ||||
| 			if err := validURI(&uri, list.name); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	options := []struct { | ||||
| 		option JWAOptions | ||||
| 		name   string | ||||
| 	}{ | ||||
| 		{m.IDTokenResponseOptions, "id_token response"}, | ||||
| 		{m.UserInfoResponseOptions, "userinfo response"}, | ||||
| 		{m.RequestObjectOptions, "request_object"}, | ||||
| 	} | ||||
| 	for _, option := range options { | ||||
| 		if err := option.option.valid(); err != nil { | ||||
| 			return fmt.Errorf("invalid JWA values for %s: %v", option.name, err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type ClientRegistrationResponse struct { | ||||
| 	ClientID                string // Required
 | ||||
| 	ClientSecret            string | ||||
| 	RegistrationAccessToken string | ||||
| 	RegistrationClientURI   string | ||||
| 	// If IsZero is true, unspecified.
 | ||||
| 	ClientIDIssuedAt time.Time | ||||
| 	// Time at which the client_secret will expire.
 | ||||
| 	// If IsZero is true, it will not expire.
 | ||||
| 	ClientSecretExpiresAt time.Time | ||||
| 
 | ||||
| 	ClientMetadata | ||||
| } | ||||
| 
 | ||||
| type encodableClientRegistrationResponse struct { | ||||
| 	ClientID                string `json:"client_id"` // Required
 | ||||
| 	ClientSecret            string `json:"client_secret,omitempty"` | ||||
| 	RegistrationAccessToken string `json:"registration_access_token,omitempty"` | ||||
| 	RegistrationClientURI   string `json:"registration_client_uri,omitempty"` | ||||
| 	ClientIDIssuedAt        int64  `json:"client_id_issued_at,omitempty"` | ||||
| 	// Time at which the client_secret will expire, in seconds since the epoch.
 | ||||
| 	// If 0 it will not expire.
 | ||||
| 	ClientSecretExpiresAt int64 `json:"client_secret_expires_at"` // Required
 | ||||
| 
 | ||||
| 	encodableClientMetadata | ||||
| } | ||||
| 
 | ||||
| func unixToSec(t time.Time) int64 { | ||||
| 	if t.IsZero() { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return t.Unix() | ||||
| } | ||||
| 
 | ||||
| func (c *ClientRegistrationResponse) MarshalJSON() ([]byte, error) { | ||||
| 	e := encodableClientRegistrationResponse{ | ||||
| 		ClientID:                c.ClientID, | ||||
| 		ClientSecret:            c.ClientSecret, | ||||
| 		RegistrationAccessToken: c.RegistrationAccessToken, | ||||
| 		RegistrationClientURI:   c.RegistrationClientURI, | ||||
| 		ClientIDIssuedAt:        unixToSec(c.ClientIDIssuedAt), | ||||
| 		ClientSecretExpiresAt:   unixToSec(c.ClientSecretExpiresAt), | ||||
| 		encodableClientMetadata: c.ClientMetadata.toEncodableStruct(), | ||||
| 	} | ||||
| 	return json.Marshal(&e) | ||||
| } | ||||
| 
 | ||||
| func secToUnix(sec int64) time.Time { | ||||
| 	if sec == 0 { | ||||
| 		return time.Time{} | ||||
| 	} | ||||
| 	return time.Unix(sec, 0) | ||||
| } | ||||
| 
 | ||||
| func (c *ClientRegistrationResponse) UnmarshalJSON(data []byte) error { | ||||
| 	var e encodableClientRegistrationResponse | ||||
| 	if err := json.Unmarshal(data, &e); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if e.ClientID == "" { | ||||
| 		return errors.New("no client_id in client registration response") | ||||
| 	} | ||||
| 	metadata, err := e.encodableClientMetadata.toStruct() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*c = ClientRegistrationResponse{ | ||||
| 		ClientID:                e.ClientID, | ||||
| 		ClientSecret:            e.ClientSecret, | ||||
| 		RegistrationAccessToken: e.RegistrationAccessToken, | ||||
| 		RegistrationClientURI:   e.RegistrationClientURI, | ||||
| 		ClientIDIssuedAt:        secToUnix(e.ClientIDIssuedAt), | ||||
| 		ClientSecretExpiresAt:   secToUnix(e.ClientSecretExpiresAt), | ||||
| 		ClientMetadata:          metadata, | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type ClientConfig struct { | ||||
| 	HTTPClient     phttp.Client | ||||
| 	Credentials    ClientCredentials | ||||
| 	Scope          []string | ||||
| 	RedirectURL    string | ||||
| 	ProviderConfig ProviderConfig | ||||
| 	KeySet         key.PublicKeySet | ||||
| } | ||||
| 
 | ||||
| func NewClient(cfg ClientConfig) (*Client, error) { | ||||
| 	// Allow empty redirect URL in the case where the client
 | ||||
| 	// only needs to verify a given token.
 | ||||
| 	ru, err := url.Parse(cfg.RedirectURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("invalid redirect URL: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	c := Client{ | ||||
| 		credentials:    cfg.Credentials, | ||||
| 		httpClient:     cfg.HTTPClient, | ||||
| 		scope:          cfg.Scope, | ||||
| 		redirectURL:    ru.String(), | ||||
| 		providerConfig: newProviderConfigRepo(cfg.ProviderConfig), | ||||
| 		keySet:         cfg.KeySet, | ||||
| 	} | ||||
| 
 | ||||
| 	if c.httpClient == nil { | ||||
| 		c.httpClient = http.DefaultClient | ||||
| 	} | ||||
| 
 | ||||
| 	if c.scope == nil { | ||||
| 		c.scope = make([]string, len(DefaultScope)) | ||||
| 		copy(c.scope, DefaultScope) | ||||
| 	} | ||||
| 
 | ||||
| 	return &c, nil | ||||
| } | ||||
| 
 | ||||
| type Client struct { | ||||
| 	httpClient     phttp.Client | ||||
| 	providerConfig *providerConfigRepo | ||||
| 	credentials    ClientCredentials | ||||
| 	redirectURL    string | ||||
| 	scope          []string | ||||
| 	keySet         key.PublicKeySet | ||||
| 	providerSyncer *ProviderConfigSyncer | ||||
| 
 | ||||
| 	keySetSyncMutex sync.RWMutex | ||||
| 	lastKeySetSync  time.Time | ||||
| } | ||||
| 
 | ||||
| func (c *Client) Healthy() error { | ||||
| 	now := time.Now().UTC() | ||||
| 
 | ||||
| 	cfg := c.providerConfig.Get() | ||||
| 
 | ||||
| 	if cfg.Empty() { | ||||
| 		return errors.New("oidc client provider config empty") | ||||
| 	} | ||||
| 
 | ||||
| 	if !cfg.ExpiresAt.IsZero() && cfg.ExpiresAt.Before(now) { | ||||
| 		return errors.New("oidc client provider config expired") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *Client) OAuthClient() (*oauth2.Client, error) { | ||||
| 	cfg := c.providerConfig.Get() | ||||
| 	authMethod, err := chooseAuthMethod(cfg) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	ocfg := oauth2.Config{ | ||||
| 		Credentials: oauth2.ClientCredentials(c.credentials), | ||||
| 		RedirectURL: c.redirectURL, | ||||
| 		AuthURL:     cfg.AuthEndpoint.String(), | ||||
| 		TokenURL:    cfg.TokenEndpoint.String(), | ||||
| 		Scope:       c.scope, | ||||
| 		AuthMethod:  authMethod, | ||||
| 	} | ||||
| 
 | ||||
| 	return oauth2.NewClient(c.httpClient, ocfg) | ||||
| } | ||||
| 
 | ||||
| func chooseAuthMethod(cfg ProviderConfig) (string, error) { | ||||
| 	if len(cfg.TokenEndpointAuthMethodsSupported) == 0 { | ||||
| 		return oauth2.AuthMethodClientSecretBasic, nil | ||||
| 	} | ||||
| 
 | ||||
| 	for _, authMethod := range cfg.TokenEndpointAuthMethodsSupported { | ||||
| 		if _, ok := supportedAuthMethods[authMethod]; ok { | ||||
| 			return authMethod, nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return "", errors.New("no supported auth methods") | ||||
| } | ||||
| 
 | ||||
| // SyncProviderConfig starts the provider config syncer
 | ||||
| func (c *Client) SyncProviderConfig(discoveryURL string) chan struct{} { | ||||
| 	r := NewHTTPProviderConfigGetter(c.httpClient, discoveryURL) | ||||
| 	s := NewProviderConfigSyncer(r, c.providerConfig) | ||||
| 	stop := s.Run() | ||||
| 	s.WaitUntilInitialSync() | ||||
| 	return stop | ||||
| } | ||||
| 
 | ||||
| func (c *Client) maybeSyncKeys() error { | ||||
| 	tooSoon := func() bool { | ||||
| 		return time.Now().UTC().Before(c.lastKeySetSync.Add(keySyncWindow)) | ||||
| 	} | ||||
| 
 | ||||
| 	// ignore request to sync keys if a sync operation has been
 | ||||
| 	// attempted too recently
 | ||||
| 	if tooSoon() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	c.keySetSyncMutex.Lock() | ||||
| 	defer c.keySetSyncMutex.Unlock() | ||||
| 
 | ||||
| 	// check again, as another goroutine may have been holding
 | ||||
| 	// the lock while updating the keys
 | ||||
| 	if tooSoon() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	cfg := c.providerConfig.Get() | ||||
| 	r := NewRemotePublicKeyRepo(c.httpClient, cfg.KeysEndpoint.String()) | ||||
| 	w := &clientKeyRepo{client: c} | ||||
| 	_, err := key.Sync(r, w) | ||||
| 	c.lastKeySetSync = time.Now().UTC() | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| type clientKeyRepo struct { | ||||
| 	client *Client | ||||
| } | ||||
| 
 | ||||
| func (r *clientKeyRepo) Set(ks key.KeySet) error { | ||||
| 	pks, ok := ks.(*key.PublicKeySet) | ||||
| 	if !ok { | ||||
| 		return errors.New("unable to cast to PublicKey") | ||||
| 	} | ||||
| 	r.client.keySet = *pks | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *Client) ClientCredsToken(scope []string) (jose.JWT, error) { | ||||
| 	cfg := c.providerConfig.Get() | ||||
| 
 | ||||
| 	if !cfg.SupportsGrantType(oauth2.GrantTypeClientCreds) { | ||||
| 		return jose.JWT{}, fmt.Errorf("%v grant type is not supported", oauth2.GrantTypeClientCreds) | ||||
| 	} | ||||
| 
 | ||||
| 	oac, err := c.OAuthClient() | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	t, err := oac.ClientCredsToken(scope) | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	jwt, err := jose.ParseJWT(t.IDToken) | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return jwt, c.VerifyJWT(jwt) | ||||
| } | ||||
| 
 | ||||
| // ExchangeAuthCode exchanges an OAuth2 auth code for an OIDC JWT ID token.
 | ||||
| func (c *Client) ExchangeAuthCode(code string) (jose.JWT, error) { | ||||
| 	oac, err := c.OAuthClient() | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	t, err := oac.RequestToken(oauth2.GrantTypeAuthCode, code) | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	jwt, err := jose.ParseJWT(t.IDToken) | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return jwt, c.VerifyJWT(jwt) | ||||
| } | ||||
| 
 | ||||
| // RefreshToken uses a refresh token to exchange for a new OIDC JWT ID Token.
 | ||||
| func (c *Client) RefreshToken(refreshToken string) (jose.JWT, error) { | ||||
| 	oac, err := c.OAuthClient() | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	t, err := oac.RequestToken(oauth2.GrantTypeRefreshToken, refreshToken) | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	jwt, err := jose.ParseJWT(t.IDToken) | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return jwt, c.VerifyJWT(jwt) | ||||
| } | ||||
| 
 | ||||
| func (c *Client) VerifyJWT(jwt jose.JWT) error { | ||||
| 	var keysFunc func() []key.PublicKey | ||||
| 	if kID, ok := jwt.KeyID(); ok { | ||||
| 		keysFunc = c.keysFuncWithID(kID) | ||||
| 	} else { | ||||
| 		keysFunc = c.keysFuncAll() | ||||
| 	} | ||||
| 
 | ||||
| 	v := NewJWTVerifier( | ||||
| 		c.providerConfig.Get().Issuer.String(), | ||||
| 		c.credentials.ID, | ||||
| 		c.maybeSyncKeys, keysFunc) | ||||
| 
 | ||||
| 	return v.Verify(jwt) | ||||
| } | ||||
| 
 | ||||
| // keysFuncWithID returns a function that retrieves at most unexpired
 | ||||
| // public key from the Client that matches the provided ID
 | ||||
| func (c *Client) keysFuncWithID(kID string) func() []key.PublicKey { | ||||
| 	return func() []key.PublicKey { | ||||
| 		c.keySetSyncMutex.RLock() | ||||
| 		defer c.keySetSyncMutex.RUnlock() | ||||
| 
 | ||||
| 		if c.keySet.ExpiresAt().Before(time.Now()) { | ||||
| 			return []key.PublicKey{} | ||||
| 		} | ||||
| 
 | ||||
| 		k := c.keySet.Key(kID) | ||||
| 		if k == nil { | ||||
| 			return []key.PublicKey{} | ||||
| 		} | ||||
| 
 | ||||
| 		return []key.PublicKey{*k} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // keysFuncAll returns a function that retrieves all unexpired public
 | ||||
| // keys from the Client
 | ||||
| func (c *Client) keysFuncAll() func() []key.PublicKey { | ||||
| 	return func() []key.PublicKey { | ||||
| 		c.keySetSyncMutex.RLock() | ||||
| 		defer c.keySetSyncMutex.RUnlock() | ||||
| 
 | ||||
| 		if c.keySet.ExpiresAt().Before(time.Now()) { | ||||
| 			return []key.PublicKey{} | ||||
| 		} | ||||
| 
 | ||||
| 		return c.keySet.Keys() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type providerConfigRepo struct { | ||||
| 	mu     sync.RWMutex | ||||
| 	config ProviderConfig // do not access directly, use Get()
 | ||||
| } | ||||
| 
 | ||||
| func newProviderConfigRepo(pc ProviderConfig) *providerConfigRepo { | ||||
| 	return &providerConfigRepo{sync.RWMutex{}, pc} | ||||
| } | ||||
| 
 | ||||
| // returns an error to implement ProviderConfigSetter
 | ||||
| func (r *providerConfigRepo) Set(cfg ProviderConfig) error { | ||||
| 	r.mu.Lock() | ||||
| 	defer r.mu.Unlock() | ||||
| 	r.config = cfg | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (r *providerConfigRepo) Get() ProviderConfig { | ||||
| 	r.mu.RLock() | ||||
| 	defer r.mu.RUnlock() | ||||
| 	return r.config | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| // Package oidc is DEPRECATED. Use github.com/coreos/go-oidc instead.
 | ||||
| package oidc | ||||
|  | @ -1,44 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| ) | ||||
| 
 | ||||
| type Identity struct { | ||||
| 	ID        string | ||||
| 	Name      string | ||||
| 	Email     string | ||||
| 	ExpiresAt time.Time | ||||
| } | ||||
| 
 | ||||
| func IdentityFromClaims(claims jose.Claims) (*Identity, error) { | ||||
| 	if claims == nil { | ||||
| 		return nil, errors.New("nil claim set") | ||||
| 	} | ||||
| 
 | ||||
| 	var ident Identity | ||||
| 	var err error | ||||
| 	var ok bool | ||||
| 
 | ||||
| 	if ident.ID, ok, err = claims.StringClaim("sub"); err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !ok { | ||||
| 		return nil, errors.New("missing required claim: sub") | ||||
| 	} | ||||
| 
 | ||||
| 	if ident.Email, _, err = claims.StringClaim("email"); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	exp, ok, err := claims.TimeClaim("exp") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if ok { | ||||
| 		ident.ExpiresAt = exp | ||||
| 	} | ||||
| 
 | ||||
| 	return &ident, nil | ||||
| } | ||||
|  | @ -1,3 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| type LoginFunc func(ident Identity, sessionKey string) (redirectURL string, err error) | ||||
|  | @ -1,67 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| 
 | ||||
| 	phttp "github.com/coreos/go-oidc/http" | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| 	"github.com/coreos/go-oidc/key" | ||||
| ) | ||||
| 
 | ||||
| // DefaultPublicKeySetTTL is the default TTL set on the PublicKeySet if no
 | ||||
| // Cache-Control header is provided by the JWK Set document endpoint.
 | ||||
| const DefaultPublicKeySetTTL = 24 * time.Hour | ||||
| 
 | ||||
| // NewRemotePublicKeyRepo is responsible for fetching the JWK Set document.
 | ||||
| func NewRemotePublicKeyRepo(hc phttp.Client, ep string) *remotePublicKeyRepo { | ||||
| 	return &remotePublicKeyRepo{hc: hc, ep: ep} | ||||
| } | ||||
| 
 | ||||
| type remotePublicKeyRepo struct { | ||||
| 	hc phttp.Client | ||||
| 	ep string | ||||
| } | ||||
| 
 | ||||
| // Get returns a PublicKeySet fetched from the JWK Set document endpoint. A TTL
 | ||||
| // is set on the Key Set to avoid it having to be re-retrieved for every
 | ||||
| // encryption event. This TTL is typically controlled by the endpoint returning
 | ||||
| // a Cache-Control header, but defaults to 24 hours if no Cache-Control header
 | ||||
| // is found.
 | ||||
| func (r *remotePublicKeyRepo) Get() (key.KeySet, error) { | ||||
| 	req, err := http.NewRequest("GET", r.ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := r.hc.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	var d struct { | ||||
| 		Keys []jose.JWK `json:"keys"` | ||||
| 	} | ||||
| 	if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if len(d.Keys) == 0 { | ||||
| 		return nil, errors.New("zero keys in response") | ||||
| 	} | ||||
| 
 | ||||
| 	ttl, ok, err := phttp.Cacheable(resp.Header) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if !ok { | ||||
| 		ttl = DefaultPublicKeySetTTL | ||||
| 	} | ||||
| 
 | ||||
| 	exp := time.Now().UTC().Add(ttl) | ||||
| 	ks := key.NewPublicKeySet(d.Keys, exp) | ||||
| 	return ks, nil | ||||
| } | ||||
|  | @ -1,687 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/coreos/pkg/timeutil" | ||||
| 	"github.com/jonboulle/clockwork" | ||||
| 
 | ||||
| 	phttp "github.com/coreos/go-oidc/http" | ||||
| 	"github.com/coreos/go-oidc/oauth2" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// Subject Identifier types defined by the OIDC spec. Specifies if the provider
 | ||||
| 	// should provide the same sub claim value to all clients (public) or a unique
 | ||||
| 	// value for each client (pairwise).
 | ||||
| 	//
 | ||||
| 	// See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
 | ||||
| 	SubjectTypePublic   = "public" | ||||
| 	SubjectTypePairwise = "pairwise" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// Default values for omitted provider config fields.
 | ||||
| 	//
 | ||||
| 	// Use ProviderConfig's Defaults method to fill a provider config with these values.
 | ||||
| 	DefaultGrantTypesSupported               = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit} | ||||
| 	DefaultResponseModesSupported            = []string{"query", "fragment"} | ||||
| 	DefaultTokenEndpointAuthMethodsSupported = []string{oauth2.AuthMethodClientSecretBasic} | ||||
| 	DefaultClaimTypesSupported               = []string{"normal"} | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	MaximumProviderConfigSyncInterval = 24 * time.Hour | ||||
| 	MinimumProviderConfigSyncInterval = time.Minute | ||||
| 
 | ||||
| 	discoveryConfigPath = "/.well-known/openid-configuration" | ||||
| ) | ||||
| 
 | ||||
| // internally configurable for tests
 | ||||
| var minimumProviderConfigSyncInterval = MinimumProviderConfigSyncInterval | ||||
| 
 | ||||
| var ( | ||||
| 	// Ensure ProviderConfig satisfies these interfaces.
 | ||||
| 	_ json.Marshaler   = &ProviderConfig{} | ||||
| 	_ json.Unmarshaler = &ProviderConfig{} | ||||
| ) | ||||
| 
 | ||||
| // ProviderConfig represents the OpenID Provider Metadata specifying what
 | ||||
| // configurations a provider supports.
 | ||||
| //
 | ||||
| // See: http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
 | ||||
| type ProviderConfig struct { | ||||
| 	Issuer               *url.URL // Required
 | ||||
| 	AuthEndpoint         *url.URL // Required
 | ||||
| 	TokenEndpoint        *url.URL // Required if grant types other than "implicit" are supported
 | ||||
| 	UserInfoEndpoint     *url.URL | ||||
| 	KeysEndpoint         *url.URL // Required
 | ||||
| 	RegistrationEndpoint *url.URL | ||||
| 	EndSessionEndpoint   *url.URL | ||||
| 	CheckSessionIFrame   *url.URL | ||||
| 
 | ||||
| 	// Servers MAY choose not to advertise some supported scope values even when this
 | ||||
| 	// parameter is used, although those defined in OpenID Core SHOULD be listed, if supported.
 | ||||
| 	ScopesSupported []string | ||||
| 	// OAuth2.0 response types supported.
 | ||||
| 	ResponseTypesSupported []string // Required
 | ||||
| 	// OAuth2.0 response modes supported.
 | ||||
| 	//
 | ||||
| 	// If omitted, defaults to DefaultResponseModesSupported.
 | ||||
| 	ResponseModesSupported []string | ||||
| 	// OAuth2.0 grant types supported.
 | ||||
| 	//
 | ||||
| 	// If omitted, defaults to DefaultGrantTypesSupported.
 | ||||
| 	GrantTypesSupported []string | ||||
| 	ACRValuesSupported  []string | ||||
| 	// SubjectTypesSupported specifies strategies for providing values for the sub claim.
 | ||||
| 	SubjectTypesSupported []string // Required
 | ||||
| 
 | ||||
| 	// JWA signing and encryption algorith values supported for ID tokens.
 | ||||
| 	IDTokenSigningAlgValues    []string // Required
 | ||||
| 	IDTokenEncryptionAlgValues []string | ||||
| 	IDTokenEncryptionEncValues []string | ||||
| 
 | ||||
| 	// JWA signing and encryption algorith values supported for user info responses.
 | ||||
| 	UserInfoSigningAlgValues    []string | ||||
| 	UserInfoEncryptionAlgValues []string | ||||
| 	UserInfoEncryptionEncValues []string | ||||
| 
 | ||||
| 	// JWA signing and encryption algorith values supported for request objects.
 | ||||
| 	ReqObjSigningAlgValues    []string | ||||
| 	ReqObjEncryptionAlgValues []string | ||||
| 	ReqObjEncryptionEncValues []string | ||||
| 
 | ||||
| 	TokenEndpointAuthMethodsSupported          []string | ||||
| 	TokenEndpointAuthSigningAlgValuesSupported []string | ||||
| 	DisplayValuesSupported                     []string | ||||
| 	ClaimTypesSupported                        []string | ||||
| 	ClaimsSupported                            []string | ||||
| 	ServiceDocs                                *url.URL | ||||
| 	ClaimsLocalsSupported                      []string | ||||
| 	UILocalsSupported                          []string | ||||
| 	ClaimsParameterSupported                   bool | ||||
| 	RequestParameterSupported                  bool | ||||
| 	RequestURIParamaterSupported               bool | ||||
| 	RequireRequestURIRegistration              bool | ||||
| 
 | ||||
| 	Policy         *url.URL | ||||
| 	TermsOfService *url.URL | ||||
| 
 | ||||
| 	// Not part of the OpenID Provider Metadata
 | ||||
| 	ExpiresAt time.Time | ||||
| } | ||||
| 
 | ||||
| // Defaults returns a shallow copy of ProviderConfig with default
 | ||||
| // values replacing omitted fields.
 | ||||
| //
 | ||||
| //     var cfg oidc.ProviderConfig
 | ||||
| //     // Fill provider config with default values for omitted fields.
 | ||||
| //     cfg = cfg.Defaults()
 | ||||
| //
 | ||||
| func (p ProviderConfig) Defaults() ProviderConfig { | ||||
| 	setDefault := func(val *[]string, defaultVal []string) { | ||||
| 		if len(*val) == 0 { | ||||
| 			*val = defaultVal | ||||
| 		} | ||||
| 	} | ||||
| 	setDefault(&p.GrantTypesSupported, DefaultGrantTypesSupported) | ||||
| 	setDefault(&p.ResponseModesSupported, DefaultResponseModesSupported) | ||||
| 	setDefault(&p.TokenEndpointAuthMethodsSupported, DefaultTokenEndpointAuthMethodsSupported) | ||||
| 	setDefault(&p.ClaimTypesSupported, DefaultClaimTypesSupported) | ||||
| 	return p | ||||
| } | ||||
| 
 | ||||
| func (p *ProviderConfig) MarshalJSON() ([]byte, error) { | ||||
| 	e := p.toEncodableStruct() | ||||
| 	return json.Marshal(&e) | ||||
| } | ||||
| 
 | ||||
| func (p *ProviderConfig) UnmarshalJSON(data []byte) error { | ||||
| 	var e encodableProviderConfig | ||||
| 	if err := json.Unmarshal(data, &e); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	conf, err := e.toStruct() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := conf.Valid(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*p = conf | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type encodableProviderConfig struct { | ||||
| 	Issuer               string `json:"issuer"` | ||||
| 	AuthEndpoint         string `json:"authorization_endpoint"` | ||||
| 	TokenEndpoint        string `json:"token_endpoint"` | ||||
| 	UserInfoEndpoint     string `json:"userinfo_endpoint,omitempty"` | ||||
| 	KeysEndpoint         string `json:"jwks_uri"` | ||||
| 	RegistrationEndpoint string `json:"registration_endpoint,omitempty"` | ||||
| 	EndSessionEndpoint   string `json:"end_session_endpoint,omitempty"` | ||||
| 	CheckSessionIFrame   string `json:"check_session_iframe,omitempty"` | ||||
| 
 | ||||
| 	// Use 'omitempty' for all slices as per OIDC spec:
 | ||||
| 	// "Claims that return multiple values are represented as JSON arrays.
 | ||||
| 	// Claims with zero elements MUST be omitted from the response."
 | ||||
| 	// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
 | ||||
| 
 | ||||
| 	ScopesSupported        []string `json:"scopes_supported,omitempty"` | ||||
| 	ResponseTypesSupported []string `json:"response_types_supported,omitempty"` | ||||
| 	ResponseModesSupported []string `json:"response_modes_supported,omitempty"` | ||||
| 	GrantTypesSupported    []string `json:"grant_types_supported,omitempty"` | ||||
| 	ACRValuesSupported     []string `json:"acr_values_supported,omitempty"` | ||||
| 	SubjectTypesSupported  []string `json:"subject_types_supported,omitempty"` | ||||
| 
 | ||||
| 	IDTokenSigningAlgValues     []string `json:"id_token_signing_alg_values_supported,omitempty"` | ||||
| 	IDTokenEncryptionAlgValues  []string `json:"id_token_encryption_alg_values_supported,omitempty"` | ||||
| 	IDTokenEncryptionEncValues  []string `json:"id_token_encryption_enc_values_supported,omitempty"` | ||||
| 	UserInfoSigningAlgValues    []string `json:"userinfo_signing_alg_values_supported,omitempty"` | ||||
| 	UserInfoEncryptionAlgValues []string `json:"userinfo_encryption_alg_values_supported,omitempty"` | ||||
| 	UserInfoEncryptionEncValues []string `json:"userinfo_encryption_enc_values_supported,omitempty"` | ||||
| 	ReqObjSigningAlgValues      []string `json:"request_object_signing_alg_values_supported,omitempty"` | ||||
| 	ReqObjEncryptionAlgValues   []string `json:"request_object_encryption_alg_values_supported,omitempty"` | ||||
| 	ReqObjEncryptionEncValues   []string `json:"request_object_encryption_enc_values_supported,omitempty"` | ||||
| 
 | ||||
| 	TokenEndpointAuthMethodsSupported          []string `json:"token_endpoint_auth_methods_supported,omitempty"` | ||||
| 	TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` | ||||
| 
 | ||||
| 	DisplayValuesSupported        []string `json:"display_values_supported,omitempty"` | ||||
| 	ClaimTypesSupported           []string `json:"claim_types_supported,omitempty"` | ||||
| 	ClaimsSupported               []string `json:"claims_supported,omitempty"` | ||||
| 	ServiceDocs                   string   `json:"service_documentation,omitempty"` | ||||
| 	ClaimsLocalsSupported         []string `json:"claims_locales_supported,omitempty"` | ||||
| 	UILocalsSupported             []string `json:"ui_locales_supported,omitempty"` | ||||
| 	ClaimsParameterSupported      bool     `json:"claims_parameter_supported,omitempty"` | ||||
| 	RequestParameterSupported     bool     `json:"request_parameter_supported,omitempty"` | ||||
| 	RequestURIParamaterSupported  bool     `json:"request_uri_parameter_supported,omitempty"` | ||||
| 	RequireRequestURIRegistration bool     `json:"require_request_uri_registration,omitempty"` | ||||
| 
 | ||||
| 	Policy         string `json:"op_policy_uri,omitempty"` | ||||
| 	TermsOfService string `json:"op_tos_uri,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func (cfg ProviderConfig) toEncodableStruct() encodableProviderConfig { | ||||
| 	return encodableProviderConfig{ | ||||
| 		Issuer:                                     uriToString(cfg.Issuer), | ||||
| 		AuthEndpoint:                               uriToString(cfg.AuthEndpoint), | ||||
| 		TokenEndpoint:                              uriToString(cfg.TokenEndpoint), | ||||
| 		UserInfoEndpoint:                           uriToString(cfg.UserInfoEndpoint), | ||||
| 		KeysEndpoint:                               uriToString(cfg.KeysEndpoint), | ||||
| 		RegistrationEndpoint:                       uriToString(cfg.RegistrationEndpoint), | ||||
| 		EndSessionEndpoint:                         uriToString(cfg.EndSessionEndpoint), | ||||
| 		CheckSessionIFrame:                         uriToString(cfg.CheckSessionIFrame), | ||||
| 		ScopesSupported:                            cfg.ScopesSupported, | ||||
| 		ResponseTypesSupported:                     cfg.ResponseTypesSupported, | ||||
| 		ResponseModesSupported:                     cfg.ResponseModesSupported, | ||||
| 		GrantTypesSupported:                        cfg.GrantTypesSupported, | ||||
| 		ACRValuesSupported:                         cfg.ACRValuesSupported, | ||||
| 		SubjectTypesSupported:                      cfg.SubjectTypesSupported, | ||||
| 		IDTokenSigningAlgValues:                    cfg.IDTokenSigningAlgValues, | ||||
| 		IDTokenEncryptionAlgValues:                 cfg.IDTokenEncryptionAlgValues, | ||||
| 		IDTokenEncryptionEncValues:                 cfg.IDTokenEncryptionEncValues, | ||||
| 		UserInfoSigningAlgValues:                   cfg.UserInfoSigningAlgValues, | ||||
| 		UserInfoEncryptionAlgValues:                cfg.UserInfoEncryptionAlgValues, | ||||
| 		UserInfoEncryptionEncValues:                cfg.UserInfoEncryptionEncValues, | ||||
| 		ReqObjSigningAlgValues:                     cfg.ReqObjSigningAlgValues, | ||||
| 		ReqObjEncryptionAlgValues:                  cfg.ReqObjEncryptionAlgValues, | ||||
| 		ReqObjEncryptionEncValues:                  cfg.ReqObjEncryptionEncValues, | ||||
| 		TokenEndpointAuthMethodsSupported:          cfg.TokenEndpointAuthMethodsSupported, | ||||
| 		TokenEndpointAuthSigningAlgValuesSupported: cfg.TokenEndpointAuthSigningAlgValuesSupported, | ||||
| 		DisplayValuesSupported:                     cfg.DisplayValuesSupported, | ||||
| 		ClaimTypesSupported:                        cfg.ClaimTypesSupported, | ||||
| 		ClaimsSupported:                            cfg.ClaimsSupported, | ||||
| 		ServiceDocs:                                uriToString(cfg.ServiceDocs), | ||||
| 		ClaimsLocalsSupported:                      cfg.ClaimsLocalsSupported, | ||||
| 		UILocalsSupported:                          cfg.UILocalsSupported, | ||||
| 		ClaimsParameterSupported:                   cfg.ClaimsParameterSupported, | ||||
| 		RequestParameterSupported:                  cfg.RequestParameterSupported, | ||||
| 		RequestURIParamaterSupported:               cfg.RequestURIParamaterSupported, | ||||
| 		RequireRequestURIRegistration:              cfg.RequireRequestURIRegistration, | ||||
| 		Policy:         uriToString(cfg.Policy), | ||||
| 		TermsOfService: uriToString(cfg.TermsOfService), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (e encodableProviderConfig) toStruct() (ProviderConfig, error) { | ||||
| 	p := stickyErrParser{} | ||||
| 	conf := ProviderConfig{ | ||||
| 		Issuer:                                     p.parseURI(e.Issuer, "issuer"), | ||||
| 		AuthEndpoint:                               p.parseURI(e.AuthEndpoint, "authorization_endpoint"), | ||||
| 		TokenEndpoint:                              p.parseURI(e.TokenEndpoint, "token_endpoint"), | ||||
| 		UserInfoEndpoint:                           p.parseURI(e.UserInfoEndpoint, "userinfo_endpoint"), | ||||
| 		KeysEndpoint:                               p.parseURI(e.KeysEndpoint, "jwks_uri"), | ||||
| 		RegistrationEndpoint:                       p.parseURI(e.RegistrationEndpoint, "registration_endpoint"), | ||||
| 		EndSessionEndpoint:                         p.parseURI(e.EndSessionEndpoint, "end_session_endpoint"), | ||||
| 		CheckSessionIFrame:                         p.parseURI(e.CheckSessionIFrame, "check_session_iframe"), | ||||
| 		ScopesSupported:                            e.ScopesSupported, | ||||
| 		ResponseTypesSupported:                     e.ResponseTypesSupported, | ||||
| 		ResponseModesSupported:                     e.ResponseModesSupported, | ||||
| 		GrantTypesSupported:                        e.GrantTypesSupported, | ||||
| 		ACRValuesSupported:                         e.ACRValuesSupported, | ||||
| 		SubjectTypesSupported:                      e.SubjectTypesSupported, | ||||
| 		IDTokenSigningAlgValues:                    e.IDTokenSigningAlgValues, | ||||
| 		IDTokenEncryptionAlgValues:                 e.IDTokenEncryptionAlgValues, | ||||
| 		IDTokenEncryptionEncValues:                 e.IDTokenEncryptionEncValues, | ||||
| 		UserInfoSigningAlgValues:                   e.UserInfoSigningAlgValues, | ||||
| 		UserInfoEncryptionAlgValues:                e.UserInfoEncryptionAlgValues, | ||||
| 		UserInfoEncryptionEncValues:                e.UserInfoEncryptionEncValues, | ||||
| 		ReqObjSigningAlgValues:                     e.ReqObjSigningAlgValues, | ||||
| 		ReqObjEncryptionAlgValues:                  e.ReqObjEncryptionAlgValues, | ||||
| 		ReqObjEncryptionEncValues:                  e.ReqObjEncryptionEncValues, | ||||
| 		TokenEndpointAuthMethodsSupported:          e.TokenEndpointAuthMethodsSupported, | ||||
| 		TokenEndpointAuthSigningAlgValuesSupported: e.TokenEndpointAuthSigningAlgValuesSupported, | ||||
| 		DisplayValuesSupported:                     e.DisplayValuesSupported, | ||||
| 		ClaimTypesSupported:                        e.ClaimTypesSupported, | ||||
| 		ClaimsSupported:                            e.ClaimsSupported, | ||||
| 		ServiceDocs:                                p.parseURI(e.ServiceDocs, "service_documentation"), | ||||
| 		ClaimsLocalsSupported:                      e.ClaimsLocalsSupported, | ||||
| 		UILocalsSupported:                          e.UILocalsSupported, | ||||
| 		ClaimsParameterSupported:                   e.ClaimsParameterSupported, | ||||
| 		RequestParameterSupported:                  e.RequestParameterSupported, | ||||
| 		RequestURIParamaterSupported:               e.RequestURIParamaterSupported, | ||||
| 		RequireRequestURIRegistration:              e.RequireRequestURIRegistration, | ||||
| 		Policy:         p.parseURI(e.Policy, "op_policy-uri"), | ||||
| 		TermsOfService: p.parseURI(e.TermsOfService, "op_tos_uri"), | ||||
| 	} | ||||
| 	if p.firstErr != nil { | ||||
| 		return ProviderConfig{}, p.firstErr | ||||
| 	} | ||||
| 	return conf, nil | ||||
| } | ||||
| 
 | ||||
| // Empty returns if a ProviderConfig holds no information.
 | ||||
| //
 | ||||
| // This case generally indicates a ProviderConfigGetter has experienced an error
 | ||||
| // and has nothing to report.
 | ||||
| func (p ProviderConfig) Empty() bool { | ||||
| 	return p.Issuer == nil | ||||
| } | ||||
| 
 | ||||
| func contains(sli []string, ele string) bool { | ||||
| 	for _, s := range sli { | ||||
| 		if s == ele { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Valid determines if a ProviderConfig conforms with the OIDC specification.
 | ||||
| // If Valid returns successfully it guarantees required field are non-nil and
 | ||||
| // URLs are well formed.
 | ||||
| //
 | ||||
| // Valid is called by UnmarshalJSON.
 | ||||
| //
 | ||||
| // NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
 | ||||
| // URLs fields where the OIDC spec requires it. This may change in future releases
 | ||||
| // of this package. See: https://github.com/coreos/go-oidc/issues/34
 | ||||
| func (p ProviderConfig) Valid() error { | ||||
| 	grantTypes := p.GrantTypesSupported | ||||
| 	if len(grantTypes) == 0 { | ||||
| 		grantTypes = DefaultGrantTypesSupported | ||||
| 	} | ||||
| 	implicitOnly := true | ||||
| 	for _, grantType := range grantTypes { | ||||
| 		if grantType != oauth2.GrantTypeImplicit { | ||||
| 			implicitOnly = false | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if len(p.SubjectTypesSupported) == 0 { | ||||
| 		return errors.New("missing required field subject_types_supported") | ||||
| 	} | ||||
| 	if len(p.IDTokenSigningAlgValues) == 0 { | ||||
| 		return errors.New("missing required field id_token_signing_alg_values_supported") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(p.ScopesSupported) != 0 && !contains(p.ScopesSupported, "openid") { | ||||
| 		return errors.New("scoped_supported must be unspecified or include 'openid'") | ||||
| 	} | ||||
| 
 | ||||
| 	if !contains(p.IDTokenSigningAlgValues, "RS256") { | ||||
| 		return errors.New("id_token_signing_alg_values_supported must include 'RS256'") | ||||
| 	} | ||||
| 
 | ||||
| 	uris := []struct { | ||||
| 		val      *url.URL | ||||
| 		name     string | ||||
| 		required bool | ||||
| 	}{ | ||||
| 		{p.Issuer, "issuer", true}, | ||||
| 		{p.AuthEndpoint, "authorization_endpoint", true}, | ||||
| 		{p.TokenEndpoint, "token_endpoint", !implicitOnly}, | ||||
| 		{p.UserInfoEndpoint, "userinfo_endpoint", false}, | ||||
| 		{p.KeysEndpoint, "jwks_uri", true}, | ||||
| 		{p.RegistrationEndpoint, "registration_endpoint", false}, | ||||
| 		{p.EndSessionEndpoint, "end_session_endpoint", false}, | ||||
| 		{p.CheckSessionIFrame, "check_session_iframe", false}, | ||||
| 		{p.ServiceDocs, "service_documentation", false}, | ||||
| 		{p.Policy, "op_policy_uri", false}, | ||||
| 		{p.TermsOfService, "op_tos_uri", false}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, uri := range uris { | ||||
| 		if uri.val == nil { | ||||
| 			if !uri.required { | ||||
| 				continue | ||||
| 			} | ||||
| 			return fmt.Errorf("empty value for required uri field %s", uri.name) | ||||
| 		} | ||||
| 		if uri.val.Host == "" { | ||||
| 			return fmt.Errorf("no host for uri field %s", uri.name) | ||||
| 		} | ||||
| 		if uri.val.Scheme != "http" && uri.val.Scheme != "https" { | ||||
| 			return fmt.Errorf("uri field %s schemeis not http or https", uri.name) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Supports determines if provider supports a client given their respective metadata.
 | ||||
| func (p ProviderConfig) Supports(c ClientMetadata) error { | ||||
| 	if err := p.Valid(); err != nil { | ||||
| 		return fmt.Errorf("invalid provider config: %v", err) | ||||
| 	} | ||||
| 	if err := c.Valid(); err != nil { | ||||
| 		return fmt.Errorf("invalid client config: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Fill default values for omitted fields
 | ||||
| 	c = c.Defaults() | ||||
| 	p = p.Defaults() | ||||
| 
 | ||||
| 	// Do the supported values list the requested one?
 | ||||
| 	supports := []struct { | ||||
| 		supported []string | ||||
| 		requested string | ||||
| 		name      string | ||||
| 	}{ | ||||
| 		{p.IDTokenSigningAlgValues, c.IDTokenResponseOptions.SigningAlg, "id_token_signed_response_alg"}, | ||||
| 		{p.IDTokenEncryptionAlgValues, c.IDTokenResponseOptions.EncryptionAlg, "id_token_encryption_response_alg"}, | ||||
| 		{p.IDTokenEncryptionEncValues, c.IDTokenResponseOptions.EncryptionEnc, "id_token_encryption_response_enc"}, | ||||
| 		{p.UserInfoSigningAlgValues, c.UserInfoResponseOptions.SigningAlg, "userinfo_signed_response_alg"}, | ||||
| 		{p.UserInfoEncryptionAlgValues, c.UserInfoResponseOptions.EncryptionAlg, "userinfo_encryption_response_alg"}, | ||||
| 		{p.UserInfoEncryptionEncValues, c.UserInfoResponseOptions.EncryptionEnc, "userinfo_encryption_response_enc"}, | ||||
| 		{p.ReqObjSigningAlgValues, c.RequestObjectOptions.SigningAlg, "request_object_signing_alg"}, | ||||
| 		{p.ReqObjEncryptionAlgValues, c.RequestObjectOptions.EncryptionAlg, "request_object_encryption_alg"}, | ||||
| 		{p.ReqObjEncryptionEncValues, c.RequestObjectOptions.EncryptionEnc, "request_object_encryption_enc"}, | ||||
| 	} | ||||
| 	for _, field := range supports { | ||||
| 		if field.requested == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		if !contains(field.supported, field.requested) { | ||||
| 			return fmt.Errorf("provider does not support requested value for field %s", field.name) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	stringsEqual := func(s1, s2 string) bool { return s1 == s2 } | ||||
| 
 | ||||
| 	// For lists, are the list of requested values a subset of the supported ones?
 | ||||
| 	supportsAll := []struct { | ||||
| 		supported []string | ||||
| 		requested []string | ||||
| 		name      string | ||||
| 		// OAuth2.0 response_type can be space separated lists where order doesn't matter.
 | ||||
| 		// For example "id_token token" is the same as "token id_token"
 | ||||
| 		// Support a custom compare method.
 | ||||
| 		comp func(s1, s2 string) bool | ||||
| 	}{ | ||||
| 		{p.GrantTypesSupported, c.GrantTypes, "grant_types", stringsEqual}, | ||||
| 		{p.ResponseTypesSupported, c.ResponseTypes, "response_type", oauth2.ResponseTypesEqual}, | ||||
| 	} | ||||
| 	for _, field := range supportsAll { | ||||
| 	requestLoop: | ||||
| 		for _, req := range field.requested { | ||||
| 			for _, sup := range field.supported { | ||||
| 				if field.comp(req, sup) { | ||||
| 					continue requestLoop | ||||
| 				} | ||||
| 			} | ||||
| 			return fmt.Errorf("provider does not support requested value for field %s", field.name) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(ericchiang): Are there more checks we feel comfortable with begin strict about?
 | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (p ProviderConfig) SupportsGrantType(grantType string) bool { | ||||
| 	var supported []string | ||||
| 	if len(p.GrantTypesSupported) == 0 { | ||||
| 		supported = DefaultGrantTypesSupported | ||||
| 	} else { | ||||
| 		supported = p.GrantTypesSupported | ||||
| 	} | ||||
| 
 | ||||
| 	for _, t := range supported { | ||||
| 		if t == grantType { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| type ProviderConfigGetter interface { | ||||
| 	Get() (ProviderConfig, error) | ||||
| } | ||||
| 
 | ||||
| type ProviderConfigSetter interface { | ||||
| 	Set(ProviderConfig) error | ||||
| } | ||||
| 
 | ||||
| type ProviderConfigSyncer struct { | ||||
| 	from  ProviderConfigGetter | ||||
| 	to    ProviderConfigSetter | ||||
| 	clock clockwork.Clock | ||||
| 
 | ||||
| 	initialSyncDone bool | ||||
| 	initialSyncWait sync.WaitGroup | ||||
| } | ||||
| 
 | ||||
| func NewProviderConfigSyncer(from ProviderConfigGetter, to ProviderConfigSetter) *ProviderConfigSyncer { | ||||
| 	return &ProviderConfigSyncer{ | ||||
| 		from:  from, | ||||
| 		to:    to, | ||||
| 		clock: clockwork.NewRealClock(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *ProviderConfigSyncer) Run() chan struct{} { | ||||
| 	stop := make(chan struct{}) | ||||
| 
 | ||||
| 	var next pcsStepper | ||||
| 	next = &pcsStepNext{aft: time.Duration(0)} | ||||
| 
 | ||||
| 	s.initialSyncWait.Add(1) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			select { | ||||
| 			case <-s.clock.After(next.after()): | ||||
| 				next = next.step(s.sync) | ||||
| 			case <-stop: | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	return stop | ||||
| } | ||||
| 
 | ||||
| func (s *ProviderConfigSyncer) WaitUntilInitialSync() { | ||||
| 	s.initialSyncWait.Wait() | ||||
| } | ||||
| 
 | ||||
| func (s *ProviderConfigSyncer) sync() (time.Duration, error) { | ||||
| 	cfg, err := s.from.Get() | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 
 | ||||
| 	if err = s.to.Set(cfg); err != nil { | ||||
| 		return 0, fmt.Errorf("error setting provider config: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !s.initialSyncDone { | ||||
| 		s.initialSyncWait.Done() | ||||
| 		s.initialSyncDone = true | ||||
| 	} | ||||
| 
 | ||||
| 	return nextSyncAfter(cfg.ExpiresAt, s.clock), nil | ||||
| } | ||||
| 
 | ||||
| type pcsStepFunc func() (time.Duration, error) | ||||
| 
 | ||||
| type pcsStepper interface { | ||||
| 	after() time.Duration | ||||
| 	step(pcsStepFunc) pcsStepper | ||||
| } | ||||
| 
 | ||||
| type pcsStepNext struct { | ||||
| 	aft time.Duration | ||||
| } | ||||
| 
 | ||||
| func (n *pcsStepNext) after() time.Duration { | ||||
| 	return n.aft | ||||
| } | ||||
| 
 | ||||
| func (n *pcsStepNext) step(fn pcsStepFunc) (next pcsStepper) { | ||||
| 	ttl, err := fn() | ||||
| 	if err == nil { | ||||
| 		next = &pcsStepNext{aft: ttl} | ||||
| 	} else { | ||||
| 		next = &pcsStepRetry{aft: time.Second} | ||||
| 		log.Printf("go-oidc: provider config sync failed, retrying in %v: %v", next.after(), err) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| type pcsStepRetry struct { | ||||
| 	aft time.Duration | ||||
| } | ||||
| 
 | ||||
| func (r *pcsStepRetry) after() time.Duration { | ||||
| 	return r.aft | ||||
| } | ||||
| 
 | ||||
| func (r *pcsStepRetry) step(fn pcsStepFunc) (next pcsStepper) { | ||||
| 	ttl, err := fn() | ||||
| 	if err == nil { | ||||
| 		next = &pcsStepNext{aft: ttl} | ||||
| 	} else { | ||||
| 		next = &pcsStepRetry{aft: timeutil.ExpBackoff(r.aft, time.Minute)} | ||||
| 		log.Printf("go-oidc: provider config sync failed, retrying in %v: %v", next.after(), err) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func nextSyncAfter(exp time.Time, clock clockwork.Clock) time.Duration { | ||||
| 	if exp.IsZero() { | ||||
| 		return MaximumProviderConfigSyncInterval | ||||
| 	} | ||||
| 
 | ||||
| 	t := exp.Sub(clock.Now()) / 2 | ||||
| 	if t > MaximumProviderConfigSyncInterval { | ||||
| 		t = MaximumProviderConfigSyncInterval | ||||
| 	} else if t < minimumProviderConfigSyncInterval { | ||||
| 		t = minimumProviderConfigSyncInterval | ||||
| 	} | ||||
| 
 | ||||
| 	return t | ||||
| } | ||||
| 
 | ||||
| type httpProviderConfigGetter struct { | ||||
| 	hc        phttp.Client | ||||
| 	issuerURL string | ||||
| 	clock     clockwork.Clock | ||||
| } | ||||
| 
 | ||||
| func NewHTTPProviderConfigGetter(hc phttp.Client, issuerURL string) *httpProviderConfigGetter { | ||||
| 	return &httpProviderConfigGetter{ | ||||
| 		hc:        hc, | ||||
| 		issuerURL: issuerURL, | ||||
| 		clock:     clockwork.NewRealClock(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) { | ||||
| 	// If the Issuer value contains a path component, any terminating / MUST be removed before
 | ||||
| 	// appending /.well-known/openid-configuration.
 | ||||
| 	// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
 | ||||
| 	discoveryURL := strings.TrimSuffix(r.issuerURL, "/") + discoveryConfigPath | ||||
| 	req, err := http.NewRequest("GET", discoveryURL, nil) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := r.hc.Do(req) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	if err = json.NewDecoder(resp.Body).Decode(&cfg); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var ttl time.Duration | ||||
| 	var ok bool | ||||
| 	ttl, ok, err = phttp.Cacheable(resp.Header) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} else if ok { | ||||
| 		cfg.ExpiresAt = r.clock.Now().UTC().Add(ttl) | ||||
| 	} | ||||
| 
 | ||||
| 	// The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information.
 | ||||
| 	// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation
 | ||||
| 	if !urlEqual(cfg.Issuer.String(), r.issuerURL) { | ||||
| 		err = fmt.Errorf(`"issuer" in config (%v) does not match provided issuer URL (%v)`, cfg.Issuer, r.issuerURL) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func FetchProviderConfig(hc phttp.Client, issuerURL string) (ProviderConfig, error) { | ||||
| 	if hc == nil { | ||||
| 		hc = http.DefaultClient | ||||
| 	} | ||||
| 
 | ||||
| 	g := NewHTTPProviderConfigGetter(hc, issuerURL) | ||||
| 	return g.Get() | ||||
| } | ||||
| 
 | ||||
| func WaitForProviderConfig(hc phttp.Client, issuerURL string) (pcfg ProviderConfig) { | ||||
| 	return waitForProviderConfig(hc, issuerURL, clockwork.NewRealClock()) | ||||
| } | ||||
| 
 | ||||
| func waitForProviderConfig(hc phttp.Client, issuerURL string, clock clockwork.Clock) (pcfg ProviderConfig) { | ||||
| 	var sleep time.Duration | ||||
| 	var err error | ||||
| 	for { | ||||
| 		pcfg, err = FetchProviderConfig(hc, issuerURL) | ||||
| 		if err == nil { | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		sleep = timeutil.ExpBackoff(sleep, time.Minute) | ||||
| 		fmt.Printf("Failed fetching provider config, trying again in %v: %v\n", sleep, err) | ||||
| 		time.Sleep(sleep) | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
|  | @ -1,88 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	phttp "github.com/coreos/go-oidc/http" | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| ) | ||||
| 
 | ||||
| type TokenRefresher interface { | ||||
| 	// Verify checks if the provided token is currently valid or not.
 | ||||
| 	Verify(jose.JWT) error | ||||
| 
 | ||||
| 	// Refresh attempts to authenticate and retrieve a new token.
 | ||||
| 	Refresh() (jose.JWT, error) | ||||
| } | ||||
| 
 | ||||
| type ClientCredsTokenRefresher struct { | ||||
| 	Issuer     string | ||||
| 	OIDCClient *Client | ||||
| } | ||||
| 
 | ||||
| func (c *ClientCredsTokenRefresher) Verify(jwt jose.JWT) (err error) { | ||||
| 	_, err = VerifyClientClaims(jwt, c.Issuer) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (c *ClientCredsTokenRefresher) Refresh() (jwt jose.JWT, err error) { | ||||
| 	if err = c.OIDCClient.Healthy(); err != nil { | ||||
| 		err = fmt.Errorf("unable to authenticate, unhealthy OIDC client: %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	jwt, err = c.OIDCClient.ClientCredsToken([]string{"openid"}) | ||||
| 	if err != nil { | ||||
| 		err = fmt.Errorf("unable to verify auth code with issuer: %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| type AuthenticatedTransport struct { | ||||
| 	TokenRefresher | ||||
| 	http.RoundTripper | ||||
| 
 | ||||
| 	mu  sync.Mutex | ||||
| 	jwt jose.JWT | ||||
| } | ||||
| 
 | ||||
| func (t *AuthenticatedTransport) verifiedJWT() (jose.JWT, error) { | ||||
| 	t.mu.Lock() | ||||
| 	defer t.mu.Unlock() | ||||
| 
 | ||||
| 	if t.TokenRefresher.Verify(t.jwt) == nil { | ||||
| 		return t.jwt, nil | ||||
| 	} | ||||
| 
 | ||||
| 	jwt, err := t.TokenRefresher.Refresh() | ||||
| 	if err != nil { | ||||
| 		return jose.JWT{}, fmt.Errorf("unable to acquire valid JWT: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	t.jwt = jwt | ||||
| 	return t.jwt, nil | ||||
| } | ||||
| 
 | ||||
| // SetJWT sets the JWT held by the Transport.
 | ||||
| // This is useful for cases in which you want to set an initial JWT.
 | ||||
| func (t *AuthenticatedTransport) SetJWT(jwt jose.JWT) { | ||||
| 	t.mu.Lock() | ||||
| 	defer t.mu.Unlock() | ||||
| 
 | ||||
| 	t.jwt = jwt | ||||
| } | ||||
| 
 | ||||
| func (t *AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) { | ||||
| 	jwt, err := t.verifiedJWT() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req := phttp.CopyRequest(r) | ||||
| 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt.Encode())) | ||||
| 	return t.RoundTripper.RoundTrip(req) | ||||
| } | ||||
|  | @ -1,109 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| ) | ||||
| 
 | ||||
| // RequestTokenExtractor funcs extract a raw encoded token from a request.
 | ||||
| type RequestTokenExtractor func(r *http.Request) (string, error) | ||||
| 
 | ||||
| // ExtractBearerToken is a RequestTokenExtractor which extracts a bearer token from a request's
 | ||||
| // Authorization header.
 | ||||
| func ExtractBearerToken(r *http.Request) (string, error) { | ||||
| 	ah := r.Header.Get("Authorization") | ||||
| 	if ah == "" { | ||||
| 		return "", errors.New("missing Authorization header") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(ah) <= 6 || strings.ToUpper(ah[0:6]) != "BEARER" { | ||||
| 		return "", errors.New("should be a bearer token") | ||||
| 	} | ||||
| 
 | ||||
| 	val := ah[7:] | ||||
| 	if len(val) == 0 { | ||||
| 		return "", errors.New("bearer token is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	return val, nil | ||||
| } | ||||
| 
 | ||||
| // CookieTokenExtractor returns a RequestTokenExtractor which extracts a token from the named cookie in a request.
 | ||||
| func CookieTokenExtractor(cookieName string) RequestTokenExtractor { | ||||
| 	return func(r *http.Request) (string, error) { | ||||
| 		ck, err := r.Cookie(cookieName) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("token cookie not found in request: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 		if ck.Value == "" { | ||||
| 			return "", errors.New("token cookie found but is empty") | ||||
| 		} | ||||
| 
 | ||||
| 		return ck.Value, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func NewClaims(iss, sub string, aud interface{}, iat, exp time.Time) jose.Claims { | ||||
| 	return jose.Claims{ | ||||
| 		// required
 | ||||
| 		"iss": iss, | ||||
| 		"sub": sub, | ||||
| 		"aud": aud, | ||||
| 		"iat": iat.Unix(), | ||||
| 		"exp": exp.Unix(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GenClientID(hostport string) (string, error) { | ||||
| 	b, err := randBytes(32) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	var host string | ||||
| 	if strings.Contains(hostport, ":") { | ||||
| 		host, _, err = net.SplitHostPort(hostport) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 	} else { | ||||
| 		host = hostport | ||||
| 	} | ||||
| 
 | ||||
| 	return fmt.Sprintf("%s@%s", base64.URLEncoding.EncodeToString(b), host), nil | ||||
| } | ||||
| 
 | ||||
| func randBytes(n int) ([]byte, error) { | ||||
| 	b := make([]byte, n) | ||||
| 	got, err := rand.Read(b) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if n != got { | ||||
| 		return nil, errors.New("unable to generate enough random data") | ||||
| 	} | ||||
| 	return b, nil | ||||
| } | ||||
| 
 | ||||
| // urlEqual checks two urls for equality using only the host and path portions.
 | ||||
| func urlEqual(url1, url2 string) bool { | ||||
| 	u1, err := url.Parse(url1) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	u2, err := url.Parse(url2) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	return strings.ToLower(u1.Host+u1.Path) == strings.ToLower(u2.Host+u2.Path) | ||||
| } | ||||
|  | @ -1,190 +0,0 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/jonboulle/clockwork" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc/jose" | ||||
| 	"github.com/coreos/go-oidc/key" | ||||
| ) | ||||
| 
 | ||||
| func VerifySignature(jwt jose.JWT, keys []key.PublicKey) (bool, error) { | ||||
| 	jwtBytes := []byte(jwt.Data()) | ||||
| 	for _, k := range keys { | ||||
| 		v, err := k.Verifier() | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 		if v.Verify(jwt.Signature, jwtBytes) == nil { | ||||
| 			return true, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return false, nil | ||||
| } | ||||
| 
 | ||||
| // containsString returns true if the given string(needle) is found
 | ||||
| // in the string array(haystack).
 | ||||
| func containsString(needle string, haystack []string) bool { | ||||
| 	for _, v := range haystack { | ||||
| 		if v == needle { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Verify claims in accordance with OIDC spec
 | ||||
| // http://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
 | ||||
| func VerifyClaims(jwt jose.JWT, issuer, clientID string) error { | ||||
| 	now := time.Now().UTC() | ||||
| 
 | ||||
| 	claims, err := jwt.Claims() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	ident, err := IdentityFromClaims(claims) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if ident.ExpiresAt.Before(now) { | ||||
| 		return errors.New("token is expired") | ||||
| 	} | ||||
| 
 | ||||
| 	// iss REQUIRED. Issuer Identifier for the Issuer of the response.
 | ||||
| 	// The iss value is a case sensitive URL using the https scheme that contains scheme,
 | ||||
| 	// host, and optionally, port number and path components and no query or fragment components.
 | ||||
| 	if iss, exists := claims["iss"].(string); exists { | ||||
| 		if !urlEqual(iss, issuer) { | ||||
| 			return fmt.Errorf("invalid claim value: 'iss'. expected=%s, found=%s.", issuer, iss) | ||||
| 		} | ||||
| 	} else { | ||||
| 		return errors.New("missing claim: 'iss'") | ||||
| 	} | ||||
| 
 | ||||
| 	// iat REQUIRED. Time at which the JWT was issued.
 | ||||
| 	// Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z
 | ||||
| 	// as measured in UTC until the date/time.
 | ||||
| 	if _, exists := claims["iat"].(float64); !exists { | ||||
| 		return errors.New("missing claim: 'iat'") | ||||
| 	} | ||||
| 
 | ||||
| 	// aud REQUIRED. Audience(s) that this ID Token is intended for.
 | ||||
| 	// It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value.
 | ||||
| 	// It MAY also contain identifiers for other audiences. In the general case, the aud
 | ||||
| 	// value is an array of case sensitive strings. In the common special case when there
 | ||||
| 	// is one audience, the aud value MAY be a single case sensitive string.
 | ||||
| 	if aud, ok, err := claims.StringClaim("aud"); err == nil && ok { | ||||
| 		if aud != clientID { | ||||
| 			return fmt.Errorf("invalid claims, 'aud' claim and 'client_id' do not match, aud=%s, client_id=%s", aud, clientID) | ||||
| 		} | ||||
| 	} else if aud, ok, err := claims.StringsClaim("aud"); err == nil && ok { | ||||
| 		if !containsString(clientID, aud) { | ||||
| 			return fmt.Errorf("invalid claims, cannot find 'client_id' in 'aud' claim, aud=%v, client_id=%s", aud, clientID) | ||||
| 		} | ||||
| 	} else { | ||||
| 		return errors.New("invalid claim value: 'aud' is required, and should be either string or string array") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // VerifyClientClaims verifies all the required claims are valid for a "client credentials" JWT.
 | ||||
| // Returns the client ID if valid, or an error if invalid.
 | ||||
| func VerifyClientClaims(jwt jose.JWT, issuer string) (string, error) { | ||||
| 	claims, err := jwt.Claims() | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to parse JWT claims: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	iss, ok, err := claims.StringClaim("iss") | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to parse 'iss' claim: %v", err) | ||||
| 	} else if !ok { | ||||
| 		return "", errors.New("missing required 'iss' claim") | ||||
| 	} else if !urlEqual(iss, issuer) { | ||||
| 		return "", fmt.Errorf("'iss' claim does not match expected issuer, iss=%s", iss) | ||||
| 	} | ||||
| 
 | ||||
| 	sub, ok, err := claims.StringClaim("sub") | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to parse 'sub' claim: %v", err) | ||||
| 	} else if !ok { | ||||
| 		return "", errors.New("missing required 'sub' claim") | ||||
| 	} | ||||
| 
 | ||||
| 	if aud, ok, err := claims.StringClaim("aud"); err == nil && ok { | ||||
| 		if aud != sub { | ||||
| 			return "", fmt.Errorf("invalid claims, 'aud' claim and 'sub' claim do not match, aud=%s, sub=%s", aud, sub) | ||||
| 		} | ||||
| 	} else if aud, ok, err := claims.StringsClaim("aud"); err == nil && ok { | ||||
| 		if !containsString(sub, aud) { | ||||
| 			return "", fmt.Errorf("invalid claims, cannot find 'sud' in 'aud' claim, aud=%v, sub=%s", aud, sub) | ||||
| 		} | ||||
| 	} else { | ||||
| 		return "", errors.New("invalid claim value: 'aud' is required, and should be either string or string array") | ||||
| 	} | ||||
| 
 | ||||
| 	now := time.Now().UTC() | ||||
| 	exp, ok, err := claims.TimeClaim("exp") | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to parse 'exp' claim: %v", err) | ||||
| 	} else if !ok { | ||||
| 		return "", errors.New("missing required 'exp' claim") | ||||
| 	} else if exp.Before(now) { | ||||
| 		return "", fmt.Errorf("token already expired at: %v", exp) | ||||
| 	} | ||||
| 
 | ||||
| 	return sub, nil | ||||
| } | ||||
| 
 | ||||
| type JWTVerifier struct { | ||||
| 	issuer   string | ||||
| 	clientID string | ||||
| 	syncFunc func() error | ||||
| 	keysFunc func() []key.PublicKey | ||||
| 	clock    clockwork.Clock | ||||
| } | ||||
| 
 | ||||
| func NewJWTVerifier(issuer, clientID string, syncFunc func() error, keysFunc func() []key.PublicKey) JWTVerifier { | ||||
| 	return JWTVerifier{ | ||||
| 		issuer:   issuer, | ||||
| 		clientID: clientID, | ||||
| 		syncFunc: syncFunc, | ||||
| 		keysFunc: keysFunc, | ||||
| 		clock:    clockwork.NewRealClock(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (v *JWTVerifier) Verify(jwt jose.JWT) error { | ||||
| 	// Verify claims before verifying the signature. This is an optimization to throw out
 | ||||
| 	// tokens we know are invalid without undergoing an expensive signature check and
 | ||||
| 	// possibly a re-sync event.
 | ||||
| 	if err := VerifyClaims(jwt, v.issuer, v.clientID); err != nil { | ||||
| 		return fmt.Errorf("oidc: JWT claims invalid: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	ok, err := VerifySignature(jwt, v.keysFunc()) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("oidc: JWT signature verification failed: %v", err) | ||||
| 	} else if ok { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if err = v.syncFunc(); err != nil { | ||||
| 		return fmt.Errorf("oidc: failed syncing KeySet: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	ok, err = VerifySignature(jwt, v.keysFunc()) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("oidc: JWT signature verification failed: %v", err) | ||||
| 	} else if !ok { | ||||
| 		return errors.New("oidc: unable to verify JWT signature: no matching keys") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| # Filter out any files with a !golint build tag. | ||||
| LINTABLE=$( go list -tags=golint -f ' | ||||
|   {{- range $i, $file := .GoFiles -}} | ||||
|     {{ $file }} {{ end }} | ||||
|   {{ range $i, $file := .TestGoFiles -}} | ||||
|     {{ $file }} {{ end }}' github.com/coreos/go-oidc ) | ||||
| 
 | ||||
| go test -v -i -race github.com/coreos/go-oidc/... | ||||
| go test -v -race github.com/coreos/go-oidc/... | ||||
| golint -set_exit_status $LINTABLE | ||||
| go vet github.com/coreos/go-oidc/... | ||||
| go build -v ./example/... | ||||
|  | @ -0,0 +1,243 @@ | |||
| package oidc | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"golang.org/x/oauth2" | ||||
| 	jose "gopkg.in/square/go-jose.v2" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	issuerGoogleAccounts         = "https://accounts.google.com" | ||||
| 	issuerGoogleAccountsNoScheme = "accounts.google.com" | ||||
| ) | ||||
| 
 | ||||
| // KeySet is a set of publc JSON Web Keys that can be used to validate the signature
 | ||||
| // of JSON web tokens. This is expected to be backed by a remote key set through
 | ||||
| // provider metadata discovery or an in-memory set of keys delivered out-of-band.
 | ||||
| type KeySet interface { | ||||
| 	// VerifySignature parses the JSON web token, verifies the signature, and returns
 | ||||
| 	// the raw payload. Header and claim fields are validated by other parts of the
 | ||||
| 	// package. For example, the KeySet does not need to check values such as signature
 | ||||
| 	// algorithm, issuer, and audience since the IDTokenVerifier validates these values
 | ||||
| 	// independently.
 | ||||
| 	//
 | ||||
| 	// If VerifySignature makes HTTP requests to verify the token, it's expected to
 | ||||
| 	// use any HTTP client associated with the context through ClientContext.
 | ||||
| 	VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) | ||||
| } | ||||
| 
 | ||||
| // IDTokenVerifier provides verification for ID Tokens.
 | ||||
| type IDTokenVerifier struct { | ||||
| 	keySet KeySet | ||||
| 	config *Config | ||||
| 	issuer string | ||||
| } | ||||
| 
 | ||||
| // NewVerifier returns a verifier manually constructed from a key set and issuer URL.
 | ||||
| //
 | ||||
| // It's easier to use provider discovery to construct an IDTokenVerifier than creating
 | ||||
| // one directly. This method is intended to be used with provider that don't support
 | ||||
| // metadata discovery, or avoiding round trips when the key set URL is already known.
 | ||||
| //
 | ||||
| // This constructor can be used to create a verifier directly using the issuer URL and
 | ||||
| // JSON Web Key Set URL without using discovery:
 | ||||
| //
 | ||||
| //		keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
 | ||||
| //		verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
 | ||||
| //
 | ||||
| // Since KeySet is an interface, this constructor can also be used to supply custom
 | ||||
| // public key sources. For example, if a user wanted to supply public keys out-of-band
 | ||||
| // and hold them statically in-memory:
 | ||||
| //
 | ||||
| //		// Custom KeySet implementation.
 | ||||
| //		keySet := newStatisKeySet(publicKeys...)
 | ||||
| //
 | ||||
| //		// Verifier uses the custom KeySet implementation.
 | ||||
| //		verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
 | ||||
| //
 | ||||
| func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier { | ||||
| 	return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL} | ||||
| } | ||||
| 
 | ||||
| // Config is the configuration for an IDTokenVerifier.
 | ||||
| type Config struct { | ||||
| 	// Expected audience of the token. For a majority of the cases this is expected to be
 | ||||
| 	// the ID of the client that initialized the login flow. It may occasionally differ if
 | ||||
| 	// the provider supports the authorizing party (azp) claim.
 | ||||
| 	//
 | ||||
| 	// If not provided, users must explicitly set SkipClientIDCheck.
 | ||||
| 	ClientID string | ||||
| 	// If specified, only this set of algorithms may be used to sign the JWT.
 | ||||
| 	//
 | ||||
| 	// Since many providers only support RS256, SupportedSigningAlgs defaults to this value.
 | ||||
| 	SupportedSigningAlgs []string | ||||
| 
 | ||||
| 	// If true, no ClientID check performed. Must be true if ClientID field is empty.
 | ||||
| 	SkipClientIDCheck bool | ||||
| 	// If true, token expiry is not checked.
 | ||||
| 	SkipExpiryCheck bool | ||||
| 
 | ||||
| 	// Time function to check Token expiry. Defaults to time.Now
 | ||||
| 	Now func() time.Time | ||||
| } | ||||
| 
 | ||||
| // Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
 | ||||
| //
 | ||||
| // The returned IDTokenVerifier is tied to the Provider's context and its behavior is
 | ||||
| // undefined once the Provider's context is canceled.
 | ||||
| func (p *Provider) Verifier(config *Config) *IDTokenVerifier { | ||||
| 	return NewVerifier(p.issuer, p.remoteKeySet, config) | ||||
| } | ||||
| 
 | ||||
| func parseJWT(p string) ([]byte, error) { | ||||
| 	parts := strings.Split(p, ".") | ||||
| 	if len(parts) < 2 { | ||||
| 		return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) | ||||
| 	} | ||||
| 	payload, err := base64.RawURLEncoding.DecodeString(parts[1]) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err) | ||||
| 	} | ||||
| 	return payload, nil | ||||
| } | ||||
| 
 | ||||
| func contains(sli []string, ele string) bool { | ||||
| 	for _, s := range sli { | ||||
| 		if s == ele { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
 | ||||
| // any additional checks depending on the Config, and returns the payload.
 | ||||
| //
 | ||||
| // Verify does NOT do nonce validation, which is the callers responsibility.
 | ||||
| //
 | ||||
| // See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
 | ||||
| //
 | ||||
| //    oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
 | ||||
| //    if err != nil {
 | ||||
| //        // handle error
 | ||||
| //    }
 | ||||
| //
 | ||||
| //    // Extract the ID Token from oauth2 token.
 | ||||
| //    rawIDToken, ok := oauth2Token.Extra("id_token").(string)
 | ||||
| //    if !ok {
 | ||||
| //        // handle error
 | ||||
| //    }
 | ||||
| //
 | ||||
| //    token, err := verifier.Verify(ctx, rawIDToken)
 | ||||
| //
 | ||||
| func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) { | ||||
| 	jws, err := jose.ParseSigned(rawIDToken) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: malformed jwt: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Throw out tokens with invalid claims before trying to verify the token. This lets
 | ||||
| 	// us do cheap checks before possibly re-syncing keys.
 | ||||
| 	payload, err := parseJWT(rawIDToken) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: malformed jwt: %v", err) | ||||
| 	} | ||||
| 	var token idToken | ||||
| 	if err := json.Unmarshal(payload, &token); err != nil { | ||||
| 		return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	t := &IDToken{ | ||||
| 		Issuer:          token.Issuer, | ||||
| 		Subject:         token.Subject, | ||||
| 		Audience:        []string(token.Audience), | ||||
| 		Expiry:          time.Time(token.Expiry), | ||||
| 		IssuedAt:        time.Time(token.IssuedAt), | ||||
| 		Nonce:           token.Nonce, | ||||
| 		AccessTokenHash: token.AtHash, | ||||
| 		claims:          payload, | ||||
| 	} | ||||
| 
 | ||||
| 	// Check issuer.
 | ||||
| 	if t.Issuer != v.issuer { | ||||
| 		// Google sometimes returns "accounts.google.com" as the issuer claim instead of
 | ||||
| 		// the required "https://accounts.google.com". Detect this case and allow it only
 | ||||
| 		// for Google.
 | ||||
| 		//
 | ||||
| 		// We will not add hooks to let other providers go off spec like this.
 | ||||
| 		if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) { | ||||
| 			return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
 | ||||
| 	//
 | ||||
| 	// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
 | ||||
| 	if !v.config.SkipClientIDCheck { | ||||
| 		if v.config.ClientID != "" { | ||||
| 			if !contains(t.Audience, v.config.ClientID) { | ||||
| 				return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience) | ||||
| 			} | ||||
| 		} else { | ||||
| 			return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// If a SkipExpiryCheck is false, make sure token is not expired.
 | ||||
| 	if !v.config.SkipExpiryCheck { | ||||
| 		now := time.Now | ||||
| 		if v.config.Now != nil { | ||||
| 			now = v.config.Now | ||||
| 		} | ||||
| 
 | ||||
| 		if t.Expiry.Before(now()) { | ||||
| 			return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	switch len(jws.Signatures) { | ||||
| 	case 0: | ||||
| 		return nil, fmt.Errorf("oidc: id token not signed") | ||||
| 	case 1: | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") | ||||
| 	} | ||||
| 
 | ||||
| 	sig := jws.Signatures[0] | ||||
| 	supportedSigAlgs := v.config.SupportedSigningAlgs | ||||
| 	if len(supportedSigAlgs) == 0 { | ||||
| 		supportedSigAlgs = []string{RS256} | ||||
| 	} | ||||
| 
 | ||||
| 	if !contains(supportedSigAlgs, sig.Header.Algorithm) { | ||||
| 		return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm) | ||||
| 	} | ||||
| 
 | ||||
| 	t.sigAlgorithm = sig.Header.Algorithm | ||||
| 
 | ||||
| 	gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to verify signature: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Ensure that the payload returned by the square actually matches the payload parsed earlier.
 | ||||
| 	if !bytes.Equal(gotPayload, payload) { | ||||
| 		return nil, errors.New("oidc: internal error, payload parsed did not match previous payload") | ||||
| 	} | ||||
| 
 | ||||
| 	return t, nil | ||||
| } | ||||
| 
 | ||||
| // Nonce returns an auth code option which requires the ID Token created by the
 | ||||
| // OpenID Connect provider to contain the specified nonce.
 | ||||
| func Nonce(nonce string) oauth2.AuthCodeOption { | ||||
| 	return oauth2.SetAuthURLParam("nonce", nonce) | ||||
| } | ||||
|  | @ -1,11 +0,0 @@ | |||
| health | ||||
| ==== | ||||
| 
 | ||||
| A simple framework for implementing an HTTP health check endpoint on servers. | ||||
| 
 | ||||
| Users implement their `health.Checkable` types, and create a `health.Checker`, from which they can get an `http.HandlerFunc` using `health.Checker.MakeHealthHandlerFunc`. | ||||
| 
 | ||||
| ### Documentation | ||||
| 
 | ||||
| For more details, visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/health) | ||||
| 
 | ||||
|  | @ -1,127 +0,0 @@ | |||
| package health | ||||
| 
 | ||||
| import ( | ||||
| 	"expvar" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/coreos/pkg/httputil" | ||||
| ) | ||||
| 
 | ||||
| // Checkables should return nil when the thing they are checking is healthy, and an error otherwise.
 | ||||
| type Checkable interface { | ||||
| 	Healthy() error | ||||
| } | ||||
| 
 | ||||
| // Checker provides a way to make an endpoint which can be probed for system health.
 | ||||
| type Checker struct { | ||||
| 	// Checks are the Checkables to be checked when probing.
 | ||||
| 	Checks []Checkable | ||||
| 
 | ||||
| 	// Unhealthyhandler is called when one or more of the checks are unhealthy.
 | ||||
| 	// If not provided DefaultUnhealthyHandler is called.
 | ||||
| 	UnhealthyHandler UnhealthyHandler | ||||
| 
 | ||||
| 	// HealthyHandler is called when all checks are healthy.
 | ||||
| 	// If not provided, DefaultHealthyHandler is called.
 | ||||
| 	HealthyHandler http.HandlerFunc | ||||
| } | ||||
| 
 | ||||
| func (c Checker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	unhealthyHandler := c.UnhealthyHandler | ||||
| 	if unhealthyHandler == nil { | ||||
| 		unhealthyHandler = DefaultUnhealthyHandler | ||||
| 	} | ||||
| 
 | ||||
| 	successHandler := c.HealthyHandler | ||||
| 	if successHandler == nil { | ||||
| 		successHandler = DefaultHealthyHandler | ||||
| 	} | ||||
| 
 | ||||
| 	if r.Method != "GET" { | ||||
| 		w.Header().Set("Allow", "GET") | ||||
| 		w.WriteHeader(http.StatusMethodNotAllowed) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := Check(c.Checks); err != nil { | ||||
| 		unhealthyHandler(w, r, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	successHandler(w, r) | ||||
| } | ||||
| 
 | ||||
| type UnhealthyHandler func(w http.ResponseWriter, r *http.Request, err error) | ||||
| 
 | ||||
| type StatusResponse struct { | ||||
| 	Status  string                 `json:"status"` | ||||
| 	Details *StatusResponseDetails `json:"details,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type StatusResponseDetails struct { | ||||
| 	Code    int    `json:"code,omitempty"` | ||||
| 	Message string `json:"message,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func Check(checks []Checkable) (err error) { | ||||
| 	errs := []error{} | ||||
| 	for _, c := range checks { | ||||
| 		if e := c.Healthy(); e != nil { | ||||
| 			errs = append(errs, e) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	switch len(errs) { | ||||
| 	case 0: | ||||
| 		err = nil | ||||
| 	case 1: | ||||
| 		err = errs[0] | ||||
| 	default: | ||||
| 		err = fmt.Errorf("multiple health check failure: %v", errs) | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func DefaultHealthyHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	err := httputil.WriteJSONResponse(w, http.StatusOK, StatusResponse{ | ||||
| 		Status: "ok", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		// TODO(bobbyrullo): replace with logging from new logging pkg,
 | ||||
| 		// once it lands.
 | ||||
| 		log.Printf("Failed to write JSON response: %v", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func DefaultUnhealthyHandler(w http.ResponseWriter, r *http.Request, err error) { | ||||
| 	writeErr := httputil.WriteJSONResponse(w, http.StatusInternalServerError, StatusResponse{ | ||||
| 		Status: "error", | ||||
| 		Details: &StatusResponseDetails{ | ||||
| 			Code:    http.StatusInternalServerError, | ||||
| 			Message: err.Error(), | ||||
| 		}, | ||||
| 	}) | ||||
| 	if writeErr != nil { | ||||
| 		// TODO(bobbyrullo): replace with logging from new logging pkg,
 | ||||
| 		// once it lands.
 | ||||
| 		log.Printf("Failed to write JSON response: %v", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ExpvarHandler is copied from https://golang.org/src/expvar/expvar.go, where it's sadly unexported.
 | ||||
| func ExpvarHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||||
| 	fmt.Fprintf(w, "{\n") | ||||
| 	first := true | ||||
| 	expvar.Do(func(kv expvar.KeyValue) { | ||||
| 		if !first { | ||||
| 			fmt.Fprintf(w, ",\n") | ||||
| 		} | ||||
| 		first = false | ||||
| 		fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) | ||||
| 	}) | ||||
| 	fmt.Fprintf(w, "\n}\n") | ||||
| } | ||||
|  | @ -1,13 +0,0 @@ | |||
| httputil | ||||
| ==== | ||||
| 
 | ||||
| Common code for dealing with HTTP. | ||||
| 
 | ||||
| Includes: | ||||
| 
 | ||||
| * Code for returning JSON responses. | ||||
| 
 | ||||
| ### Documentation | ||||
| 
 | ||||
| Visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/httputil) | ||||
| 
 | ||||
|  | @ -1,21 +0,0 @@ | |||
| package httputil | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // DeleteCookies effectively deletes all named cookies
 | ||||
| // by wiping all data and setting to expire immediately.
 | ||||
| func DeleteCookies(w http.ResponseWriter, cookieNames ...string) { | ||||
| 	for _, n := range cookieNames { | ||||
| 		c := &http.Cookie{ | ||||
| 			Name:    n, | ||||
| 			Value:   "", | ||||
| 			Path:    "/", | ||||
| 			MaxAge:  -1, | ||||
| 			Expires: time.Time{}, | ||||
| 		} | ||||
| 		http.SetCookie(w, c) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,27 +0,0 @@ | |||
| package httputil | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	JSONContentType = "application/json" | ||||
| ) | ||||
| 
 | ||||
| func WriteJSONResponse(w http.ResponseWriter, code int, resp interface{}) error { | ||||
| 	enc, err := json.Marshal(resp) | ||||
| 	if err != nil { | ||||
| 		w.WriteHeader(http.StatusInternalServerError) | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	w.Header().Set("Content-Type", JSONContentType) | ||||
| 	w.WriteHeader(code) | ||||
| 
 | ||||
| 	_, err = w.Write(enc) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,15 +0,0 @@ | |||
| package timeutil | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func ExpBackoff(prev, max time.Duration) time.Duration { | ||||
| 	if prev == 0 { | ||||
| 		return time.Second | ||||
| 	} | ||||
| 	if prev > max/2 { | ||||
| 		return max | ||||
| 	} | ||||
| 	return 2 * prev | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| language: go | ||||
| 
 | ||||
| install: | ||||
|   - go get -d -v ./... | ||||
|   - go get -u github.com/stretchr/testify/require | ||||
| 
 | ||||
| go: | ||||
|   - 1.7 | ||||
|   - 1.8 | ||||
|   - tip | ||||
|  | @ -0,0 +1,202 @@ | |||
| 
 | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | @ -0,0 +1,107 @@ | |||
| # cachecontrol: HTTP Caching Parser and Interpretation | ||||
| 
 | ||||
| [](https://godoc.org/github.com/pquerna/cachecontrol)[](https://travis-ci.org/pquerna/cachecontrol) | ||||
| 
 | ||||
|   | ||||
| 
 | ||||
| `cachecontrol` implements [RFC 7234](http://tools.ietf.org/html/rfc7234) __Hypertext Transfer Protocol (HTTP/1.1): Caching__.  It does this by parsing the `Cache-Control` and other headers, providing information about requests and responses -- but `cachecontrol` does not implement an actual cache backend, just the control plane to make decisions about if a particular response is cachable. | ||||
| 
 | ||||
| # Usage | ||||
| 
 | ||||
| `cachecontrol.CachableResponse` returns an array of [reasons](https://godoc.org/github.com/pquerna/cachecontrol/cacheobject#Reason) why a response should not be cached and when it expires.  In the case that `len(reasons) == 0`, the response is cachable according to the RFC.  However, some people want non-compliant caches for various business use cases, so each reason is specifically named, so if your cache wants to cache `POST` requests, it can easily do that, but still be RFC compliant in other situations. | ||||
| 
 | ||||
| # Examples | ||||
| 
 | ||||
| ## Can you cache Example.com? | ||||
| 
 | ||||
| ```go | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/pquerna/cachecontrol" | ||||
| 
 | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 	req, _ := http.NewRequest("GET", "http://www.example.com/", nil) | ||||
| 
 | ||||
| 	res, _ := http.DefaultClient.Do(req) | ||||
| 	_, _ = ioutil.ReadAll(res.Body) | ||||
| 
 | ||||
| 	reasons, expires, _ := cachecontrol.CachableResponse(req, res, cachecontrol.Options{}) | ||||
| 
 | ||||
| 	fmt.Println("Reasons to not cache: ", reasons) | ||||
| 	fmt.Println("Expiration: ", expires.String()) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Can I use this in a high performance caching server? | ||||
| 
 | ||||
| `cachecontrol` is divided into two packages: `cachecontrol` with a high level API, and a lower level `cacheobject` package.  Use [Object](https://godoc.org/github.com/pquerna/cachecontrol/cacheobject#Object) in a high performance use case where you have previously parsed headers containing dates or would like to avoid memory allocations. | ||||
| 
 | ||||
| ```go | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/pquerna/cachecontrol/cacheobject" | ||||
| 
 | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 	req, _ := http.NewRequest("GET", "http://www.example.com/", nil) | ||||
| 
 | ||||
| 	res, _ := http.DefaultClient.Do(req) | ||||
| 	_, _ = ioutil.ReadAll(res.Body) | ||||
| 
 | ||||
| 	reqDir, _ := cacheobject.ParseRequestCacheControl(req.Header.Get("Cache-Control")) | ||||
| 
 | ||||
| 	resDir, _ := cacheobject.ParseResponseCacheControl(res.Header.Get("Cache-Control")) | ||||
| 	expiresHeader, _ := http.ParseTime(res.Header.Get("Expires")) | ||||
| 	dateHeader, _ := http.ParseTime(res.Header.Get("Date")) | ||||
| 	lastModifiedHeader, _ := http.ParseTime(res.Header.Get("Last-Modified")) | ||||
| 
 | ||||
| 	obj := cacheobject.Object{ | ||||
| 		RespDirectives:         resDir, | ||||
| 		RespHeaders:            res.Header, | ||||
| 		RespStatusCode:         res.StatusCode, | ||||
| 		RespExpiresHeader:      expiresHeader, | ||||
| 		RespDateHeader:         dateHeader, | ||||
| 		RespLastModifiedHeader: lastModifiedHeader, | ||||
| 
 | ||||
| 		ReqDirectives: reqDir, | ||||
| 		ReqHeaders:    req.Header, | ||||
| 		ReqMethod:     req.Method, | ||||
| 
 | ||||
| 		NowUTC: time.Now().UTC(), | ||||
| 	} | ||||
| 	rv := cacheobject.ObjectResults{} | ||||
| 
 | ||||
| 	cacheobject.CachableObject(&obj, &rv) | ||||
| 	cacheobject.ExpirationObject(&obj, &rv) | ||||
| 
 | ||||
| 	fmt.Println("Errors: ", rv.OutErr) | ||||
| 	fmt.Println("Reasons to not cache: ", rv.OutReasons) | ||||
| 	fmt.Println("Warning headers to add: ", rv.OutWarnings) | ||||
| 	fmt.Println("Expiration: ", rv.OutExpirationTime.String()) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Improvements, bugs, adding features, and taking cachecontrol new directions! | ||||
| 
 | ||||
| Please [open issues in Github](https://github.com/pquerna/cachecontrol/issues) for ideas, bugs, and general thoughts.  Pull requests are of course preferred :) | ||||
| 
 | ||||
| # Credits | ||||
| 
 | ||||
| `cachecontrol` has recieved significant contributions from: | ||||
| 
 | ||||
| * [Paul Querna](https://github.com/pquerna)  | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| `cachecontrol` is licensed under the [Apache License, Version 2.0](./LICENSE) | ||||
|  | @ -0,0 +1,48 @@ | |||
| /** | ||||
|  *  Copyright 2015 Paul Querna | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| package cachecontrol | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/pquerna/cachecontrol/cacheobject" | ||||
| 
 | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Options struct { | ||||
| 	// Set to True for a prviate cache, which is not shared amoung users (eg, in a browser)
 | ||||
| 	// Set to False for a "shared" cache, which is more common in a server context.
 | ||||
| 	PrivateCache bool | ||||
| } | ||||
| 
 | ||||
| // Given an HTTP Request, the future Status Code, and an ResponseWriter,
 | ||||
| // determine the possible reasons a response SHOULD NOT be cached.
 | ||||
| func CachableResponseWriter(req *http.Request, | ||||
| 	statusCode int, | ||||
| 	resp http.ResponseWriter, | ||||
| 	opts Options) ([]cacheobject.Reason, time.Time, error) { | ||||
| 	return cacheobject.UsingRequestResponse(req, statusCode, resp.Header(), opts.PrivateCache) | ||||
| } | ||||
| 
 | ||||
| // Given an HTTP Request and Response, determine the possible reasons a response SHOULD NOT
 | ||||
| // be cached.
 | ||||
| func CachableResponse(req *http.Request, | ||||
| 	resp *http.Response, | ||||
| 	opts Options) ([]cacheobject.Reason, time.Time, error) { | ||||
| 	return cacheobject.UsingRequestResponse(req, resp.StatusCode, resp.Header, opts.PrivateCache) | ||||
| } | ||||
|  | @ -0,0 +1,510 @@ | |||
| /** | ||||
|  *  Copyright 2015 Paul Querna | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| package cacheobject | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"math" | ||||
| 	"net/http" | ||||
| 	"net/textproto" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // TODO(pquerna): add extensions from here: http://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
 | ||||
| 
 | ||||
| var ( | ||||
| 	ErrQuoteMismatch         = errors.New("Missing closing quote") | ||||
| 	ErrMaxAgeDeltaSeconds    = errors.New("Failed to parse delta-seconds in `max-age`") | ||||
| 	ErrSMaxAgeDeltaSeconds   = errors.New("Failed to parse delta-seconds in `s-maxage`") | ||||
| 	ErrMaxStaleDeltaSeconds  = errors.New("Failed to parse delta-seconds in `min-fresh`") | ||||
| 	ErrMinFreshDeltaSeconds  = errors.New("Failed to parse delta-seconds in `min-fresh`") | ||||
| 	ErrNoCacheNoArgs         = errors.New("Unexpected argument to `no-cache`") | ||||
| 	ErrNoStoreNoArgs         = errors.New("Unexpected argument to `no-store`") | ||||
| 	ErrNoTransformNoArgs     = errors.New("Unexpected argument to `no-transform`") | ||||
| 	ErrOnlyIfCachedNoArgs    = errors.New("Unexpected argument to `only-if-cached`") | ||||
| 	ErrMustRevalidateNoArgs  = errors.New("Unexpected argument to `must-revalidate`") | ||||
| 	ErrPublicNoArgs          = errors.New("Unexpected argument to `public`") | ||||
| 	ErrProxyRevalidateNoArgs = errors.New("Unexpected argument to `proxy-revalidate`") | ||||
| ) | ||||
| 
 | ||||
| func whitespace(b byte) bool { | ||||
| 	if b == '\t' || b == ' ' { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func parse(value string, cd cacheDirective) error { | ||||
| 	var err error = nil | ||||
| 	i := 0 | ||||
| 
 | ||||
| 	for i < len(value) && err == nil { | ||||
| 		// eat leading whitespace or commas
 | ||||
| 		if whitespace(value[i]) || value[i] == ',' { | ||||
| 			i++ | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		j := i + 1 | ||||
| 
 | ||||
| 		for j < len(value) { | ||||
| 			if !isToken(value[j]) { | ||||
| 				break | ||||
| 			} | ||||
| 			j++ | ||||
| 		} | ||||
| 
 | ||||
| 		token := strings.ToLower(value[i:j]) | ||||
| 		tokenHasFields := hasFieldNames(token) | ||||
| 		/* | ||||
| 			println("GOT TOKEN:") | ||||
| 			println("	i -> ", i) | ||||
| 			println("	j -> ", j) | ||||
| 			println("	token -> ", token) | ||||
| 		*/ | ||||
| 
 | ||||
| 		if j+1 < len(value) && value[j] == '=' { | ||||
| 			k := j + 1 | ||||
| 			// minimum size two bytes of "", but we let httpUnquote handle it.
 | ||||
| 			if k < len(value) && value[k] == '"' { | ||||
| 				eaten, result := httpUnquote(value[k:]) | ||||
| 				if eaten == -1 { | ||||
| 					return ErrQuoteMismatch | ||||
| 				} | ||||
| 				i = k + eaten | ||||
| 
 | ||||
| 				err = cd.addPair(token, result) | ||||
| 			} else { | ||||
| 				z := k | ||||
| 				for z < len(value) { | ||||
| 					if tokenHasFields { | ||||
| 						if whitespace(value[z]) { | ||||
| 							break | ||||
| 						} | ||||
| 					} else { | ||||
| 						if whitespace(value[z]) || value[z] == ',' { | ||||
| 							break | ||||
| 						} | ||||
| 					} | ||||
| 					z++ | ||||
| 				} | ||||
| 				i = z | ||||
| 
 | ||||
| 				result := value[k:z] | ||||
| 				if result != "" && result[len(result)-1] == ',' { | ||||
| 					result = result[:len(result)-1] | ||||
| 				} | ||||
| 
 | ||||
| 				err = cd.addPair(token, result) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if token != "," { | ||||
| 				err = cd.addToken(token) | ||||
| 			} | ||||
| 			i = j | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // DeltaSeconds specifies a non-negative integer, representing
 | ||||
| // time in seconds: http://tools.ietf.org/html/rfc7234#section-1.2.1
 | ||||
| //
 | ||||
| // When set to -1, this means unset.
 | ||||
| //
 | ||||
| type DeltaSeconds int32 | ||||
| 
 | ||||
| // Parser for delta-seconds, a uint31, more or less:
 | ||||
| // http://tools.ietf.org/html/rfc7234#section-1.2.1
 | ||||
| func parseDeltaSeconds(v string) (DeltaSeconds, error) { | ||||
| 	n, err := strconv.ParseUint(v, 10, 32) | ||||
| 	if err != nil { | ||||
| 		if numError, ok := err.(*strconv.NumError); ok { | ||||
| 			if numError.Err == strconv.ErrRange { | ||||
| 				return DeltaSeconds(math.MaxInt32), nil | ||||
| 			} | ||||
| 		} | ||||
| 		return DeltaSeconds(-1), err | ||||
| 	} else { | ||||
| 		if n > math.MaxInt32 { | ||||
| 			return DeltaSeconds(math.MaxInt32), nil | ||||
| 		} else { | ||||
| 			return DeltaSeconds(n), nil | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Fields present in a header.
 | ||||
| type FieldNames map[string]bool | ||||
| 
 | ||||
| // internal interface for shared methods of RequestCacheDirectives and ResponseCacheDirectives
 | ||||
| type cacheDirective interface { | ||||
| 	addToken(s string) error | ||||
| 	addPair(s string, v string) error | ||||
| } | ||||
| 
 | ||||
| // LOW LEVEL API: Repersentation of possible request directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.1
 | ||||
| //
 | ||||
| // Note: Many fields will be `nil` in practice.
 | ||||
| //
 | ||||
| type RequestCacheDirectives struct { | ||||
| 
 | ||||
| 	// max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.1
 | ||||
| 	//
 | ||||
| 	// The "max-age" request directive indicates that the client is
 | ||||
| 	// unwilling to accept a response whose age is greater than the
 | ||||
| 	// specified number of seconds.  Unless the max-stale request directive
 | ||||
| 	// is also present, the client is not willing to accept a stale
 | ||||
| 	// response.
 | ||||
| 	MaxAge DeltaSeconds | ||||
| 
 | ||||
| 	// max-stale(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.2
 | ||||
| 	//
 | ||||
| 	// The "max-stale" request directive indicates that the client is
 | ||||
| 	// willing to accept a response that has exceeded its freshness
 | ||||
| 	// lifetime.  If max-stale is assigned a value, then the client is
 | ||||
| 	// willing to accept a response that has exceeded its freshness lifetime
 | ||||
| 	// by no more than the specified number of seconds.  If no value is
 | ||||
| 	// assigned to max-stale, then the client is willing to accept a stale
 | ||||
| 	// response of any age.
 | ||||
| 	MaxStale DeltaSeconds | ||||
| 
 | ||||
| 	// min-fresh(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.3
 | ||||
| 	//
 | ||||
| 	// The "min-fresh" request directive indicates that the client is
 | ||||
| 	// willing to accept a response whose freshness lifetime is no less than
 | ||||
| 	// its current age plus the specified time in seconds.  That is, the
 | ||||
| 	// client wants a response that will still be fresh for at least the
 | ||||
| 	// specified number of seconds.
 | ||||
| 	MinFresh DeltaSeconds | ||||
| 
 | ||||
| 	// no-cache(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.4
 | ||||
| 	//
 | ||||
| 	// The "no-cache" request directive indicates that a cache MUST NOT use
 | ||||
| 	// a stored response to satisfy the request without successful
 | ||||
| 	// validation on the origin server.
 | ||||
| 	NoCache bool | ||||
| 
 | ||||
| 	// no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.5
 | ||||
| 	//
 | ||||
| 	// The "no-store" request directive indicates that a cache MUST NOT
 | ||||
| 	// store any part of either this request or any response to it.  This
 | ||||
| 	// directive applies to both private and shared caches.
 | ||||
| 	NoStore bool | ||||
| 
 | ||||
| 	// no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.6
 | ||||
| 	//
 | ||||
| 	// The "no-transform" request directive indicates that an intermediary
 | ||||
| 	// (whether or not it implements a cache) MUST NOT transform the
 | ||||
| 	// payload, as defined in Section 5.7.2 of RFC7230.
 | ||||
| 	NoTransform bool | ||||
| 
 | ||||
| 	// only-if-cached(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.7
 | ||||
| 	//
 | ||||
| 	// The "only-if-cached" request directive indicates that the client only
 | ||||
| 	// wishes to obtain a stored response.
 | ||||
| 	OnlyIfCached bool | ||||
| 
 | ||||
| 	// Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
 | ||||
| 	//
 | ||||
| 	// The Cache-Control header field can be extended through the use of one
 | ||||
| 	// or more cache-extension tokens, each with an optional value.  A cache
 | ||||
| 	// MUST ignore unrecognized cache directives.
 | ||||
| 	Extensions []string | ||||
| } | ||||
| 
 | ||||
| func (cd *RequestCacheDirectives) addToken(token string) error { | ||||
| 	var err error = nil | ||||
| 
 | ||||
| 	switch token { | ||||
| 	case "max-age": | ||||
| 		err = ErrMaxAgeDeltaSeconds | ||||
| 	case "max-stale": | ||||
| 		err = ErrMaxStaleDeltaSeconds | ||||
| 	case "min-fresh": | ||||
| 		err = ErrMinFreshDeltaSeconds | ||||
| 	case "no-cache": | ||||
| 		cd.NoCache = true | ||||
| 	case "no-store": | ||||
| 		cd.NoStore = true | ||||
| 	case "no-transform": | ||||
| 		cd.NoTransform = true | ||||
| 	case "only-if-cached": | ||||
| 		cd.OnlyIfCached = true | ||||
| 	default: | ||||
| 		cd.Extensions = append(cd.Extensions, token) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (cd *RequestCacheDirectives) addPair(token string, v string) error { | ||||
| 	var err error = nil | ||||
| 
 | ||||
| 	switch token { | ||||
| 	case "max-age": | ||||
| 		cd.MaxAge, err = parseDeltaSeconds(v) | ||||
| 		if err != nil { | ||||
| 			err = ErrMaxAgeDeltaSeconds | ||||
| 		} | ||||
| 	case "max-stale": | ||||
| 		cd.MaxStale, err = parseDeltaSeconds(v) | ||||
| 		if err != nil { | ||||
| 			err = ErrMaxStaleDeltaSeconds | ||||
| 		} | ||||
| 	case "min-fresh": | ||||
| 		cd.MinFresh, err = parseDeltaSeconds(v) | ||||
| 		if err != nil { | ||||
| 			err = ErrMinFreshDeltaSeconds | ||||
| 		} | ||||
| 	case "no-cache": | ||||
| 		err = ErrNoCacheNoArgs | ||||
| 	case "no-store": | ||||
| 		err = ErrNoStoreNoArgs | ||||
| 	case "no-transform": | ||||
| 		err = ErrNoTransformNoArgs | ||||
| 	case "only-if-cached": | ||||
| 		err = ErrOnlyIfCachedNoArgs | ||||
| 	default: | ||||
| 		// TODO(pquerna): this sucks, making user re-parse
 | ||||
| 		cd.Extensions = append(cd.Extensions, token+"="+v) | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // LOW LEVEL API: Parses a Cache Control Header from a Request into a set of directives.
 | ||||
| func ParseRequestCacheControl(value string) (*RequestCacheDirectives, error) { | ||||
| 	cd := &RequestCacheDirectives{ | ||||
| 		MaxAge:   -1, | ||||
| 		MaxStale: -1, | ||||
| 		MinFresh: -1, | ||||
| 	} | ||||
| 
 | ||||
| 	err := parse(value, cd) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return cd, nil | ||||
| } | ||||
| 
 | ||||
| // LOW LEVEL API: Repersentation of possible response directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.2
 | ||||
| //
 | ||||
| // Note: Many fields will be `nil` in practice.
 | ||||
| //
 | ||||
| type ResponseCacheDirectives struct { | ||||
| 
 | ||||
| 	// must-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.1
 | ||||
| 	//
 | ||||
| 	// The "must-revalidate" response directive indicates that once it has
 | ||||
| 	// become stale, a cache MUST NOT use the response to satisfy subsequent
 | ||||
| 	// requests without successful validation on the origin server.
 | ||||
| 	MustRevalidate bool | ||||
| 
 | ||||
| 	// no-cache(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
 | ||||
| 	//
 | ||||
| 	// The "no-cache" response directive indicates that the response MUST
 | ||||
| 	// NOT be used to satisfy a subsequent request without successful
 | ||||
| 	// validation on the origin server.
 | ||||
| 	//
 | ||||
| 	// If the no-cache response directive specifies one or more field-names,
 | ||||
| 	// then a cache MAY use the response to satisfy a subsequent request,
 | ||||
| 	// subject to any other restrictions on caching.  However, any header
 | ||||
| 	// fields in the response that have the field-name(s) listed MUST NOT be
 | ||||
| 	// sent in the response to a subsequent request without successful
 | ||||
| 	// revalidation with the origin server.
 | ||||
| 	NoCache FieldNames | ||||
| 
 | ||||
| 	// no-cache(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
 | ||||
| 	//
 | ||||
| 	// While the RFC defines optional field-names on a no-cache directive,
 | ||||
| 	// many applications only want to know if any no-cache directives were
 | ||||
| 	// present at all.
 | ||||
| 	NoCachePresent bool | ||||
| 
 | ||||
| 	// no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.3
 | ||||
| 	//
 | ||||
| 	// The "no-store" request directive indicates that a cache MUST NOT
 | ||||
| 	// store any part of either this request or any response to it.  This
 | ||||
| 	// directive applies to both private and shared caches.
 | ||||
| 	NoStore bool | ||||
| 
 | ||||
| 	// no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.4
 | ||||
| 	//
 | ||||
| 	// The "no-transform" response directive indicates that an intermediary
 | ||||
| 	// (regardless of whether it implements a cache) MUST NOT transform the
 | ||||
| 	// payload, as defined in Section 5.7.2 of RFC7230.
 | ||||
| 	NoTransform bool | ||||
| 
 | ||||
| 	// public(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.5
 | ||||
| 	//
 | ||||
| 	// The "public" response directive indicates that any cache MAY store
 | ||||
| 	// the response, even if the response would normally be non-cacheable or
 | ||||
| 	// cacheable only within a private cache.
 | ||||
| 	Public bool | ||||
| 
 | ||||
| 	// private(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
 | ||||
| 	//
 | ||||
| 	// The "private" response directive indicates that the response message
 | ||||
| 	// is intended for a single user and MUST NOT be stored by a shared
 | ||||
| 	// cache.  A private cache MAY store the response and reuse it for later
 | ||||
| 	// requests, even if the response would normally be non-cacheable.
 | ||||
| 	//
 | ||||
| 	// If the private response directive specifies one or more field-names,
 | ||||
| 	// this requirement is limited to the field-values associated with the
 | ||||
| 	// listed response header fields.  That is, a shared cache MUST NOT
 | ||||
| 	// store the specified field-names(s), whereas it MAY store the
 | ||||
| 	// remainder of the response message.
 | ||||
| 	Private FieldNames | ||||
| 
 | ||||
| 	// private(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
 | ||||
| 	//
 | ||||
| 	// While the RFC defines optional field-names on a private directive,
 | ||||
| 	// many applications only want to know if any private directives were
 | ||||
| 	// present at all.
 | ||||
| 	PrivatePresent bool | ||||
| 
 | ||||
| 	// proxy-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.7
 | ||||
| 	//
 | ||||
| 	// The "proxy-revalidate" response directive has the same meaning as the
 | ||||
| 	// must-revalidate response directive, except that it does not apply to
 | ||||
| 	// private caches.
 | ||||
| 	ProxyRevalidate bool | ||||
| 
 | ||||
| 	// max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.8
 | ||||
| 	//
 | ||||
| 	// The "max-age" response directive indicates that the response is to be
 | ||||
| 	// considered stale after its age is greater than the specified number
 | ||||
| 	// of seconds.
 | ||||
| 	MaxAge DeltaSeconds | ||||
| 
 | ||||
| 	// s-maxage(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.9
 | ||||
| 	//
 | ||||
| 	// The "s-maxage" response directive indicates that, in shared caches,
 | ||||
| 	// the maximum age specified by this directive overrides the maximum age
 | ||||
| 	// specified by either the max-age directive or the Expires header
 | ||||
| 	// field.  The s-maxage directive also implies the semantics of the
 | ||||
| 	// proxy-revalidate response directive.
 | ||||
| 	SMaxAge DeltaSeconds | ||||
| 
 | ||||
| 	// Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
 | ||||
| 	//
 | ||||
| 	// The Cache-Control header field can be extended through the use of one
 | ||||
| 	// or more cache-extension tokens, each with an optional value.  A cache
 | ||||
| 	// MUST ignore unrecognized cache directives.
 | ||||
| 	Extensions []string | ||||
| } | ||||
| 
 | ||||
| // LOW LEVEL API: Parses a Cache Control Header from a Response into a set of directives.
 | ||||
| func ParseResponseCacheControl(value string) (*ResponseCacheDirectives, error) { | ||||
| 	cd := &ResponseCacheDirectives{ | ||||
| 		MaxAge:  -1, | ||||
| 		SMaxAge: -1, | ||||
| 	} | ||||
| 
 | ||||
| 	err := parse(value, cd) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return cd, nil | ||||
| } | ||||
| 
 | ||||
| func (cd *ResponseCacheDirectives) addToken(token string) error { | ||||
| 	var err error = nil | ||||
| 	switch token { | ||||
| 	case "must-revalidate": | ||||
| 		cd.MustRevalidate = true | ||||
| 	case "no-cache": | ||||
| 		cd.NoCachePresent = true | ||||
| 	case "no-store": | ||||
| 		cd.NoStore = true | ||||
| 	case "no-transform": | ||||
| 		cd.NoTransform = true | ||||
| 	case "public": | ||||
| 		cd.Public = true | ||||
| 	case "private": | ||||
| 		cd.PrivatePresent = true | ||||
| 	case "proxy-revalidate": | ||||
| 		cd.ProxyRevalidate = true | ||||
| 	case "max-age": | ||||
| 		err = ErrMaxAgeDeltaSeconds | ||||
| 	case "s-maxage": | ||||
| 		err = ErrSMaxAgeDeltaSeconds | ||||
| 	default: | ||||
| 		cd.Extensions = append(cd.Extensions, token) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func hasFieldNames(token string) bool { | ||||
| 	switch token { | ||||
| 	case "no-cache": | ||||
| 		return true | ||||
| 	case "private": | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func (cd *ResponseCacheDirectives) addPair(token string, v string) error { | ||||
| 	var err error = nil | ||||
| 
 | ||||
| 	switch token { | ||||
| 	case "must-revalidate": | ||||
| 		err = ErrMustRevalidateNoArgs | ||||
| 	case "no-cache": | ||||
| 		cd.NoCachePresent = true | ||||
| 		tokens := strings.Split(v, ",") | ||||
| 		if cd.NoCache == nil { | ||||
| 			cd.NoCache = make(FieldNames) | ||||
| 		} | ||||
| 		for _, t := range tokens { | ||||
| 			k := http.CanonicalHeaderKey(textproto.TrimString(t)) | ||||
| 			cd.NoCache[k] = true | ||||
| 		} | ||||
| 	case "no-store": | ||||
| 		err = ErrNoStoreNoArgs | ||||
| 	case "no-transform": | ||||
| 		err = ErrNoTransformNoArgs | ||||
| 	case "public": | ||||
| 		err = ErrPublicNoArgs | ||||
| 	case "private": | ||||
| 		cd.PrivatePresent = true | ||||
| 		tokens := strings.Split(v, ",") | ||||
| 		if cd.Private == nil { | ||||
| 			cd.Private = make(FieldNames) | ||||
| 		} | ||||
| 		for _, t := range tokens { | ||||
| 			k := http.CanonicalHeaderKey(textproto.TrimString(t)) | ||||
| 			cd.Private[k] = true | ||||
| 		} | ||||
| 	case "proxy-revalidate": | ||||
| 		err = ErrProxyRevalidateNoArgs | ||||
| 	case "max-age": | ||||
| 		cd.MaxAge, err = parseDeltaSeconds(v) | ||||
| 	case "s-maxage": | ||||
| 		cd.SMaxAge, err = parseDeltaSeconds(v) | ||||
| 	default: | ||||
| 		// TODO(pquerna): this sucks, making user re-parse, and its technically not 'quoted' like the original,
 | ||||
| 		// but this is still easier, just a SplitN on "="
 | ||||
| 		cd.Extensions = append(cd.Extensions, token+"="+v) | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
|  | @ -0,0 +1,93 @@ | |||
| // Copyright 2009 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package cacheobject | ||||
| 
 | ||||
| // This file deals with lexical matters of HTTP
 | ||||
| 
 | ||||
| func isSeparator(c byte) bool { | ||||
| 	switch c { | ||||
| 	case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t': | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func isCtl(c byte) bool { return (0 <= c && c <= 31) || c == 127 } | ||||
| 
 | ||||
| func isChar(c byte) bool { return 0 <= c && c <= 127 } | ||||
| 
 | ||||
| func isAnyText(c byte) bool { return !isCtl(c) } | ||||
| 
 | ||||
| func isQdText(c byte) bool { return isAnyText(c) && c != '"' } | ||||
| 
 | ||||
| func isToken(c byte) bool { return isChar(c) && !isCtl(c) && !isSeparator(c) } | ||||
| 
 | ||||
| // Valid escaped sequences are not specified in RFC 2616, so for now, we assume
 | ||||
| // that they coincide with the common sense ones used by GO. Malformed
 | ||||
| // characters should probably not be treated as errors by a robust (forgiving)
 | ||||
| // parser, so we replace them with the '?' character.
 | ||||
| func httpUnquotePair(b byte) byte { | ||||
| 	// skip the first byte, which should always be '\'
 | ||||
| 	switch b { | ||||
| 	case 'a': | ||||
| 		return '\a' | ||||
| 	case 'b': | ||||
| 		return '\b' | ||||
| 	case 'f': | ||||
| 		return '\f' | ||||
| 	case 'n': | ||||
| 		return '\n' | ||||
| 	case 'r': | ||||
| 		return '\r' | ||||
| 	case 't': | ||||
| 		return '\t' | ||||
| 	case 'v': | ||||
| 		return '\v' | ||||
| 	case '\\': | ||||
| 		return '\\' | ||||
| 	case '\'': | ||||
| 		return '\'' | ||||
| 	case '"': | ||||
| 		return '"' | ||||
| 	} | ||||
| 	return '?' | ||||
| } | ||||
| 
 | ||||
| // raw must begin with a valid quoted string. Only the first quoted string is
 | ||||
| // parsed and is unquoted in result. eaten is the number of bytes parsed, or -1
 | ||||
| // upon failure.
 | ||||
| func httpUnquote(raw string) (eaten int, result string) { | ||||
| 	buf := make([]byte, len(raw)) | ||||
| 	if raw[0] != '"' { | ||||
| 		return -1, "" | ||||
| 	} | ||||
| 	eaten = 1 | ||||
| 	j := 0 // # of bytes written in buf
 | ||||
| 	for i := 1; i < len(raw); i++ { | ||||
| 		switch b := raw[i]; b { | ||||
| 		case '"': | ||||
| 			eaten++ | ||||
| 			buf = buf[0:j] | ||||
| 			return i + 1, string(buf) | ||||
| 		case '\\': | ||||
| 			if len(raw) < i+2 { | ||||
| 				return -1, "" | ||||
| 			} | ||||
| 			buf[j] = httpUnquotePair(raw[i+1]) | ||||
| 			eaten += 2 | ||||
| 			j++ | ||||
| 			i++ | ||||
| 		default: | ||||
| 			if isQdText(b) { | ||||
| 				buf[j] = b | ||||
| 			} else { | ||||
| 				buf[j] = '?' | ||||
| 			} | ||||
| 			eaten++ | ||||
| 			j++ | ||||
| 		} | ||||
| 	} | ||||
| 	return -1, "" | ||||
| } | ||||
|  | @ -0,0 +1,378 @@ | |||
| /** | ||||
|  *  Copyright 2015 Paul Querna | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| package cacheobject | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // LOW LEVEL API: Repersents a potentially cachable HTTP object.
 | ||||
| //
 | ||||
| // This struct is designed to be serialized efficiently, so in a high
 | ||||
| // performance caching server, things like Date-Strings don't need to be
 | ||||
| // parsed for every use of a cached object.
 | ||||
| type Object struct { | ||||
| 	CacheIsPrivate bool | ||||
| 
 | ||||
| 	RespDirectives         *ResponseCacheDirectives | ||||
| 	RespHeaders            http.Header | ||||
| 	RespStatusCode         int | ||||
| 	RespExpiresHeader      time.Time | ||||
| 	RespDateHeader         time.Time | ||||
| 	RespLastModifiedHeader time.Time | ||||
| 
 | ||||
| 	ReqDirectives *RequestCacheDirectives | ||||
| 	ReqHeaders    http.Header | ||||
| 	ReqMethod     string | ||||
| 
 | ||||
| 	NowUTC time.Time | ||||
| } | ||||
| 
 | ||||
| // LOW LEVEL API: Repersents the results of examinig an Object with
 | ||||
| // CachableObject and ExpirationObject.
 | ||||
| //
 | ||||
| // TODO(pquerna): decide if this is a good idea or bad
 | ||||
| type ObjectResults struct { | ||||
| 	OutReasons        []Reason | ||||
| 	OutWarnings       []Warning | ||||
| 	OutExpirationTime time.Time | ||||
| 	OutErr            error | ||||
| } | ||||
| 
 | ||||
| // LOW LEVEL API: Check if a object is cachable.
 | ||||
| func CachableObject(obj *Object, rv *ObjectResults) { | ||||
| 	rv.OutReasons = nil | ||||
| 	rv.OutWarnings = nil | ||||
| 	rv.OutErr = nil | ||||
| 
 | ||||
| 	switch obj.ReqMethod { | ||||
| 	case "GET": | ||||
| 		break | ||||
| 	case "HEAD": | ||||
| 		break | ||||
| 	case "POST": | ||||
| 		/** | ||||
| 		  POST: http://tools.ietf.org/html/rfc7231#section-4.3.3
 | ||||
| 
 | ||||
| 		  Responses to POST requests are only cacheable when they include | ||||
| 		  explicit freshness information (see Section 4.2.1 of [RFC7234]). | ||||
| 		  However, POST caching is not widely implemented.  For cases where an | ||||
| 		  origin server wishes the client to be able to cache the result of a | ||||
| 		  POST in a way that can be reused by a later GET, the origin server | ||||
| 		  MAY send a 200 (OK) response containing the result and a | ||||
| 		  Content-Location header field that has the same value as the POST's | ||||
| 		  effective request URI (Section 3.1.4.2). | ||||
| 		*/ | ||||
| 		if !hasFreshness(obj.ReqDirectives, obj.RespDirectives, obj.RespHeaders, obj.RespExpiresHeader, obj.CacheIsPrivate) { | ||||
| 			rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPOST) | ||||
| 		} | ||||
| 
 | ||||
| 	case "PUT": | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPUT) | ||||
| 
 | ||||
| 	case "DELETE": | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodDELETE) | ||||
| 
 | ||||
| 	case "CONNECT": | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodCONNECT) | ||||
| 
 | ||||
| 	case "OPTIONS": | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodOPTIONS) | ||||
| 
 | ||||
| 	case "TRACE": | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodTRACE) | ||||
| 
 | ||||
| 	// HTTP Extension Methods: http://www.iana.org/assignments/http-methods/http-methods.xhtml
 | ||||
| 	//
 | ||||
| 	// To my knowledge, none of them are cachable. Please open a ticket if this is not the case!
 | ||||
| 	//
 | ||||
| 	default: | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodUnkown) | ||||
| 	} | ||||
| 
 | ||||
| 	if obj.ReqDirectives.NoStore { | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonRequestNoStore) | ||||
| 	} | ||||
| 
 | ||||
| 	// Storing Responses to Authenticated Requests: http://tools.ietf.org/html/rfc7234#section-3.2
 | ||||
| 	authz := obj.ReqHeaders.Get("Authorization") | ||||
| 	if authz != "" { | ||||
| 		if obj.RespDirectives.MustRevalidate || | ||||
| 			obj.RespDirectives.Public || | ||||
| 			obj.RespDirectives.SMaxAge != -1 { | ||||
| 			// Expires of some kind present, this is potentially OK.
 | ||||
| 		} else { | ||||
| 			rv.OutReasons = append(rv.OutReasons, ReasonRequestAuthorizationHeader) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if obj.RespDirectives.PrivatePresent && !obj.CacheIsPrivate { | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonResponsePrivate) | ||||
| 	} | ||||
| 
 | ||||
| 	if obj.RespDirectives.NoStore { | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonResponseNoStore) | ||||
| 	} | ||||
| 
 | ||||
| 	/* | ||||
| 	   the response either: | ||||
| 
 | ||||
| 	     *  contains an Expires header field (see Section 5.3), or | ||||
| 
 | ||||
| 	     *  contains a max-age response directive (see Section 5.2.2.8), or | ||||
| 
 | ||||
| 	     *  contains a s-maxage response directive (see Section 5.2.2.9) | ||||
| 	        and the cache is shared, or | ||||
| 
 | ||||
| 	     *  contains a Cache Control Extension (see Section 5.2.3) that | ||||
| 	        allows it to be cached, or | ||||
| 
 | ||||
| 	     *  has a status code that is defined as cacheable by default (see | ||||
| 	        Section 4.2.2), or | ||||
| 
 | ||||
| 	     *  contains a public response directive (see Section 5.2.2.5). | ||||
| 	*/ | ||||
| 
 | ||||
| 	expires := obj.RespHeaders.Get("Expires") != "" | ||||
| 	statusCachable := cachableStatusCode(obj.RespStatusCode) | ||||
| 
 | ||||
| 	if expires || | ||||
| 		obj.RespDirectives.MaxAge != -1 || | ||||
| 		(obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate) || | ||||
| 		statusCachable || | ||||
| 		obj.RespDirectives.Public { | ||||
| 		/* cachable by default, at least one of the above conditions was true */ | ||||
| 	} else { | ||||
| 		rv.OutReasons = append(rv.OutReasons, ReasonResponseUncachableByDefault) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| var twentyFourHours = time.Duration(24 * time.Hour) | ||||
| 
 | ||||
| const debug = false | ||||
| 
 | ||||
| // LOW LEVEL API: Update an objects expiration time.
 | ||||
| func ExpirationObject(obj *Object, rv *ObjectResults) { | ||||
| 	/** | ||||
| 	 * Okay, lets calculate Freshness/Expiration now. woo: | ||||
| 	 *  http://tools.ietf.org/html/rfc7234#section-4.2
 | ||||
| 	 */ | ||||
| 
 | ||||
| 	/* | ||||
| 	   o  If the cache is shared and the s-maxage response directive | ||||
| 	      (Section 5.2.2.9) is present, use its value, or | ||||
| 
 | ||||
| 	   o  If the max-age response directive (Section 5.2.2.8) is present, | ||||
| 	      use its value, or | ||||
| 
 | ||||
| 	   o  If the Expires response header field (Section 5.3) is present, use | ||||
| 	      its value minus the value of the Date response header field, or | ||||
| 
 | ||||
| 	   o  Otherwise, no explicit expiration time is present in the response. | ||||
| 	      A heuristic freshness lifetime might be applicable; see | ||||
| 	      Section 4.2.2. | ||||
| 	*/ | ||||
| 
 | ||||
| 	var expiresTime time.Time | ||||
| 
 | ||||
| 	if obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate { | ||||
| 		expiresTime = obj.NowUTC.Add(time.Second * time.Duration(obj.RespDirectives.SMaxAge)) | ||||
| 	} else if obj.RespDirectives.MaxAge != -1 { | ||||
| 		expiresTime = obj.NowUTC.UTC().Add(time.Second * time.Duration(obj.RespDirectives.MaxAge)) | ||||
| 	} else if !obj.RespExpiresHeader.IsZero() { | ||||
| 		serverDate := obj.RespDateHeader | ||||
| 		if serverDate.IsZero() { | ||||
| 			// common enough case when a Date: header has not yet been added to an
 | ||||
| 			// active response.
 | ||||
| 			serverDate = obj.NowUTC | ||||
| 		} | ||||
| 		expiresTime = obj.NowUTC.Add(obj.RespExpiresHeader.Sub(serverDate)) | ||||
| 	} else if !obj.RespLastModifiedHeader.IsZero() { | ||||
| 		// heuristic freshness lifetime
 | ||||
| 		rv.OutWarnings = append(rv.OutWarnings, WarningHeuristicExpiration) | ||||
| 
 | ||||
| 		// http://httpd.apache.org/docs/2.4/mod/mod_cache.html#cachelastmodifiedfactor
 | ||||
| 		// CacheMaxExpire defaults to 24 hours
 | ||||
| 		// CacheLastModifiedFactor: is 0.1
 | ||||
| 		//
 | ||||
| 		// expiry-period = MIN(time-since-last-modified-date * factor, 24 hours)
 | ||||
| 		//
 | ||||
| 		// obj.NowUTC
 | ||||
| 
 | ||||
| 		since := obj.RespLastModifiedHeader.Sub(obj.NowUTC) | ||||
| 		since = time.Duration(float64(since) * -0.1) | ||||
| 
 | ||||
| 		if since > twentyFourHours { | ||||
| 			expiresTime = obj.NowUTC.Add(twentyFourHours) | ||||
| 		} else { | ||||
| 			expiresTime = obj.NowUTC.Add(since) | ||||
| 		} | ||||
| 
 | ||||
| 		if debug { | ||||
| 			println("Now UTC: ", obj.NowUTC.String()) | ||||
| 			println("Last-Modified: ", obj.RespLastModifiedHeader.String()) | ||||
| 			println("Since: ", since.String()) | ||||
| 			println("TwentyFourHours: ", twentyFourHours.String()) | ||||
| 			println("Expiration: ", expiresTime.String()) | ||||
| 		} | ||||
| 	} else { | ||||
| 		// TODO(pquerna): what should the default behavoir be for expiration time?
 | ||||
| 	} | ||||
| 
 | ||||
| 	rv.OutExpirationTime = expiresTime | ||||
| } | ||||
| 
 | ||||
| // Evaluate cachability based on an HTTP request, and parts of the response.
 | ||||
| func UsingRequestResponse(req *http.Request, | ||||
| 	statusCode int, | ||||
| 	respHeaders http.Header, | ||||
| 	privateCache bool) ([]Reason, time.Time, error) { | ||||
| 
 | ||||
| 	var reqHeaders http.Header | ||||
| 	var reqMethod string | ||||
| 
 | ||||
| 	var reqDir *RequestCacheDirectives = nil | ||||
| 	respDir, err := ParseResponseCacheControl(respHeaders.Get("Cache-Control")) | ||||
| 	if err != nil { | ||||
| 		return nil, time.Time{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	if req != nil { | ||||
| 		reqDir, err = ParseRequestCacheControl(req.Header.Get("Cache-Control")) | ||||
| 		if err != nil { | ||||
| 			return nil, time.Time{}, err | ||||
| 		} | ||||
| 		reqHeaders = req.Header | ||||
| 		reqMethod = req.Method | ||||
| 	} | ||||
| 
 | ||||
| 	var expiresHeader time.Time | ||||
| 	var dateHeader time.Time | ||||
| 	var lastModifiedHeader time.Time | ||||
| 
 | ||||
| 	if respHeaders.Get("Expires") != "" { | ||||
| 		expiresHeader, err = http.ParseTime(respHeaders.Get("Expires")) | ||||
| 		if err != nil { | ||||
| 			// sometimes servers will return `Expires: 0` or `Expires: -1` to
 | ||||
| 			// indicate expired content
 | ||||
| 			expiresHeader = time.Time{} | ||||
| 		} | ||||
| 		expiresHeader = expiresHeader.UTC() | ||||
| 	} | ||||
| 
 | ||||
| 	if respHeaders.Get("Date") != "" { | ||||
| 		dateHeader, err = http.ParseTime(respHeaders.Get("Date")) | ||||
| 		if err != nil { | ||||
| 			return nil, time.Time{}, err | ||||
| 		} | ||||
| 		dateHeader = dateHeader.UTC() | ||||
| 	} | ||||
| 
 | ||||
| 	if respHeaders.Get("Last-Modified") != "" { | ||||
| 		lastModifiedHeader, err = http.ParseTime(respHeaders.Get("Last-Modified")) | ||||
| 		if err != nil { | ||||
| 			return nil, time.Time{}, err | ||||
| 		} | ||||
| 		lastModifiedHeader = lastModifiedHeader.UTC() | ||||
| 	} | ||||
| 
 | ||||
| 	obj := Object{ | ||||
| 		CacheIsPrivate: privateCache, | ||||
| 
 | ||||
| 		RespDirectives:         respDir, | ||||
| 		RespHeaders:            respHeaders, | ||||
| 		RespStatusCode:         statusCode, | ||||
| 		RespExpiresHeader:      expiresHeader, | ||||
| 		RespDateHeader:         dateHeader, | ||||
| 		RespLastModifiedHeader: lastModifiedHeader, | ||||
| 
 | ||||
| 		ReqDirectives: reqDir, | ||||
| 		ReqHeaders:    reqHeaders, | ||||
| 		ReqMethod:     reqMethod, | ||||
| 
 | ||||
| 		NowUTC: time.Now().UTC(), | ||||
| 	} | ||||
| 	rv := ObjectResults{} | ||||
| 
 | ||||
| 	CachableObject(&obj, &rv) | ||||
| 	if rv.OutErr != nil { | ||||
| 		return nil, time.Time{}, rv.OutErr | ||||
| 	} | ||||
| 
 | ||||
| 	ExpirationObject(&obj, &rv) | ||||
| 	if rv.OutErr != nil { | ||||
| 		return nil, time.Time{}, rv.OutErr | ||||
| 	} | ||||
| 
 | ||||
| 	return rv.OutReasons, rv.OutExpirationTime, nil | ||||
| } | ||||
| 
 | ||||
| // calculate if a freshness directive is present: http://tools.ietf.org/html/rfc7234#section-4.2.1
 | ||||
| func hasFreshness(reqDir *RequestCacheDirectives, respDir *ResponseCacheDirectives, respHeaders http.Header, respExpires time.Time, privateCache bool) bool { | ||||
| 	if !privateCache && respDir.SMaxAge != -1 { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	if respDir.MaxAge != -1 { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	if !respExpires.IsZero() || respHeaders.Get("Expires") != "" { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func cachableStatusCode(statusCode int) bool { | ||||
| 	/* | ||||
| 		Responses with status codes that are defined as cacheable by default | ||||
| 		(e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in | ||||
| 		this specification) can be reused by a cache with heuristic | ||||
| 		expiration unless otherwise indicated by the method definition or | ||||
| 		explicit cache controls [RFC7234]; all other status codes are not | ||||
| 		cacheable by default. | ||||
| 	*/ | ||||
| 	switch statusCode { | ||||
| 	case 200: | ||||
| 		return true | ||||
| 	case 203: | ||||
| 		return true | ||||
| 	case 204: | ||||
| 		return true | ||||
| 	case 206: | ||||
| 		return true | ||||
| 	case 300: | ||||
| 		return true | ||||
| 	case 301: | ||||
| 		return true | ||||
| 	case 404: | ||||
| 		return true | ||||
| 	case 405: | ||||
| 		return true | ||||
| 	case 410: | ||||
| 		return true | ||||
| 	case 414: | ||||
| 		return true | ||||
| 	case 501: | ||||
| 		return true | ||||
| 	default: | ||||
| 		return false | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,95 @@ | |||
| /** | ||||
|  *  Copyright 2015 Paul Querna | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| package cacheobject | ||||
| 
 | ||||
| // Repersents a potential Reason to not cache an object.
 | ||||
| //
 | ||||
| // Applications may wish to ignore specific reasons, which will make them non-RFC
 | ||||
| // compliant, but this type gives them specific cases they can choose to ignore,
 | ||||
| // making them compliant in as many cases as they can.
 | ||||
| type Reason int | ||||
| 
 | ||||
| const ( | ||||
| 
 | ||||
| 	// The request method was POST and an Expiration header was not supplied.
 | ||||
| 	ReasonRequestMethodPOST Reason = iota | ||||
| 
 | ||||
| 	// The request method was PUT and PUTs are not cachable.
 | ||||
| 	ReasonRequestMethodPUT | ||||
| 
 | ||||
| 	// The request method was DELETE and DELETEs are not cachable.
 | ||||
| 	ReasonRequestMethodDELETE | ||||
| 
 | ||||
| 	// The request method was CONNECT and CONNECTs are not cachable.
 | ||||
| 	ReasonRequestMethodCONNECT | ||||
| 
 | ||||
| 	// The request method was OPTIONS and OPTIONS are not cachable.
 | ||||
| 	ReasonRequestMethodOPTIONS | ||||
| 
 | ||||
| 	// The request method was TRACE and TRACEs are not cachable.
 | ||||
| 	ReasonRequestMethodTRACE | ||||
| 
 | ||||
| 	// The request method was not recognized by cachecontrol, and should not be cached.
 | ||||
| 	ReasonRequestMethodUnkown | ||||
| 
 | ||||
| 	// The request included an Cache-Control: no-store header
 | ||||
| 	ReasonRequestNoStore | ||||
| 
 | ||||
| 	// The request included an Authorization header without an explicit Public or Expiration time: http://tools.ietf.org/html/rfc7234#section-3.2
 | ||||
| 	ReasonRequestAuthorizationHeader | ||||
| 
 | ||||
| 	// The response included an Cache-Control: no-store header
 | ||||
| 	ReasonResponseNoStore | ||||
| 
 | ||||
| 	// The response included an Cache-Control: private header and this is not a Private cache
 | ||||
| 	ReasonResponsePrivate | ||||
| 
 | ||||
| 	// The response failed to meet at least one of the conditions specified in RFC 7234 section 3: http://tools.ietf.org/html/rfc7234#section-3
 | ||||
| 	ReasonResponseUncachableByDefault | ||||
| ) | ||||
| 
 | ||||
| func (r Reason) String() string { | ||||
| 	switch r { | ||||
| 	case ReasonRequestMethodPOST: | ||||
| 		return "ReasonRequestMethodPOST" | ||||
| 	case ReasonRequestMethodPUT: | ||||
| 		return "ReasonRequestMethodPUT" | ||||
| 	case ReasonRequestMethodDELETE: | ||||
| 		return "ReasonRequestMethodDELETE" | ||||
| 	case ReasonRequestMethodCONNECT: | ||||
| 		return "ReasonRequestMethodCONNECT" | ||||
| 	case ReasonRequestMethodOPTIONS: | ||||
| 		return "ReasonRequestMethodOPTIONS" | ||||
| 	case ReasonRequestMethodTRACE: | ||||
| 		return "ReasonRequestMethodTRACE" | ||||
| 	case ReasonRequestMethodUnkown: | ||||
| 		return "ReasonRequestMethodUnkown" | ||||
| 	case ReasonRequestNoStore: | ||||
| 		return "ReasonRequestNoStore" | ||||
| 	case ReasonRequestAuthorizationHeader: | ||||
| 		return "ReasonRequestAuthorizationHeader" | ||||
| 	case ReasonResponseNoStore: | ||||
| 		return "ReasonResponseNoStore" | ||||
| 	case ReasonResponsePrivate: | ||||
| 		return "ReasonResponsePrivate" | ||||
| 	case ReasonResponseUncachableByDefault: | ||||
| 		return "ReasonResponseUncachableByDefault" | ||||
| 	} | ||||
| 
 | ||||
| 	panic(r) | ||||
| } | ||||
|  | @ -0,0 +1,107 @@ | |||
| /** | ||||
|  *  Copyright 2015 Paul Querna | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| package cacheobject | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // Repersents an HTTP Warning: http://tools.ietf.org/html/rfc7234#section-5.5
 | ||||
| type Warning int | ||||
| 
 | ||||
| const ( | ||||
| 	// Response is Stale
 | ||||
| 	// A cache SHOULD generate this whenever the sent response is stale.
 | ||||
| 	WarningResponseIsStale Warning = 110 | ||||
| 
 | ||||
| 	// Revalidation Failed
 | ||||
| 	// A cache SHOULD generate this when sending a stale
 | ||||
| 	// response because an attempt to validate the response failed, due to an
 | ||||
| 	// inability to reach the server.
 | ||||
| 	WarningRevalidationFailed Warning = 111 | ||||
| 
 | ||||
| 	// Disconnected Operation
 | ||||
| 	// A cache SHOULD generate this if it is intentionally disconnected from
 | ||||
| 	// the rest of the network for a period of time.
 | ||||
| 	WarningDisconnectedOperation Warning = 112 | ||||
| 
 | ||||
| 	// Heuristic Expiration
 | ||||
| 	//
 | ||||
| 	// A cache SHOULD generate this if it heuristically chose a freshness
 | ||||
| 	// lifetime greater than 24 hours and the response's age is greater than
 | ||||
| 	// 24 hours.
 | ||||
| 	WarningHeuristicExpiration Warning = 113 | ||||
| 
 | ||||
| 	// Miscellaneous Warning
 | ||||
| 	//
 | ||||
| 	// The warning text can include arbitrary information to be presented to
 | ||||
| 	// a human user or logged.  A system receiving this warning MUST NOT
 | ||||
| 	// take any automated action, besides presenting the warning to the
 | ||||
| 	// user.
 | ||||
| 	WarningMiscellaneousWarning Warning = 199 | ||||
| 
 | ||||
| 	// Transformation Applied
 | ||||
| 	//
 | ||||
| 	// This Warning code MUST be added by a proxy if it applies any
 | ||||
| 	// transformation to the representation, such as changing the
 | ||||
| 	// content-coding, media-type, or modifying the representation data,
 | ||||
| 	// unless this Warning code already appears in the response.
 | ||||
| 	WarningTransformationApplied Warning = 214 | ||||
| 
 | ||||
| 	// Miscellaneous Persistent Warning
 | ||||
| 	//
 | ||||
| 	// The warning text can include arbitrary information to be presented to
 | ||||
| 	// a human user or logged.  A system receiving this warning MUST NOT
 | ||||
| 	// take any automated action.
 | ||||
| 	WarningMiscellaneousPersistentWarning Warning = 299 | ||||
| ) | ||||
| 
 | ||||
| func (w Warning) HeaderString(agent string, date time.Time) string { | ||||
| 	if agent == "" { | ||||
| 		agent = "-" | ||||
| 	} else { | ||||
| 		// TODO(pquerna): this doesn't escape agent if it contains bad things.
 | ||||
| 		agent = `"` + agent + `"` | ||||
| 	} | ||||
| 	return fmt.Sprintf(`%d %s "%s" %s`, w, agent, w.String(), date.Format(http.TimeFormat)) | ||||
| } | ||||
| 
 | ||||
| func (w Warning) String() string { | ||||
| 	switch w { | ||||
| 	case WarningResponseIsStale: | ||||
| 		return "Response is Stale" | ||||
| 	case WarningRevalidationFailed: | ||||
| 		return "Revalidation Failed" | ||||
| 	case WarningDisconnectedOperation: | ||||
| 		return "Disconnected Operation" | ||||
| 	case WarningHeuristicExpiration: | ||||
| 		return "Heuristic Expiration" | ||||
| 	case WarningMiscellaneousWarning: | ||||
| 		// TODO(pquerna): ideally had a better way to override this one code.
 | ||||
| 		return "Miscellaneous Warning" | ||||
| 	case WarningTransformationApplied: | ||||
| 		return "Transformation Applied" | ||||
| 	case WarningMiscellaneousPersistentWarning: | ||||
| 		// TODO(pquerna): same as WarningMiscellaneousWarning
 | ||||
| 		return "Miscellaneous Persistent Warning" | ||||
| 	} | ||||
| 
 | ||||
| 	panic(w) | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| /** | ||||
|  *  Copyright 2015 Paul Querna | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| // Package cachecontrol implements the logic for HTTP Caching
 | ||||
| //
 | ||||
| // Deciding if an HTTP Response can be cached is often harder
 | ||||
| // and more bug prone than an actual cache storage backend.
 | ||||
| // cachecontrol provides a simple interface to determine if
 | ||||
| // request and response pairs are cachable as defined under
 | ||||
| // RFC 7234 http://tools.ietf.org/html/rfc7234
 | ||||
| package cachecontrol | ||||
|  | @ -0,0 +1,181 @@ | |||
| // Copyright 2016 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // Package ed25519 implements the Ed25519 signature algorithm. See
 | ||||
| // https://ed25519.cr.yp.to/.
 | ||||
| //
 | ||||
| // These functions are also compatible with the “Ed25519” function defined in
 | ||||
| // https://tools.ietf.org/html/draft-irtf-cfrg-eddsa-05.
 | ||||
| package ed25519 | ||||
| 
 | ||||
| // This code is a port of the public domain, “ref10” implementation of ed25519
 | ||||
| // from SUPERCOP.
 | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto" | ||||
| 	cryptorand "crypto/rand" | ||||
| 	"crypto/sha512" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"golang.org/x/crypto/ed25519/internal/edwards25519" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// PublicKeySize is the size, in bytes, of public keys as used in this package.
 | ||||
| 	PublicKeySize = 32 | ||||
| 	// PrivateKeySize is the size, in bytes, of private keys as used in this package.
 | ||||
| 	PrivateKeySize = 64 | ||||
| 	// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
 | ||||
| 	SignatureSize = 64 | ||||
| ) | ||||
| 
 | ||||
| // PublicKey is the type of Ed25519 public keys.
 | ||||
| type PublicKey []byte | ||||
| 
 | ||||
| // PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer.
 | ||||
| type PrivateKey []byte | ||||
| 
 | ||||
| // Public returns the PublicKey corresponding to priv.
 | ||||
| func (priv PrivateKey) Public() crypto.PublicKey { | ||||
| 	publicKey := make([]byte, PublicKeySize) | ||||
| 	copy(publicKey, priv[32:]) | ||||
| 	return PublicKey(publicKey) | ||||
| } | ||||
| 
 | ||||
| // Sign signs the given message with priv.
 | ||||
| // Ed25519 performs two passes over messages to be signed and therefore cannot
 | ||||
| // handle pre-hashed messages. Thus opts.HashFunc() must return zero to
 | ||||
| // indicate the message hasn't been hashed. This can be achieved by passing
 | ||||
| // crypto.Hash(0) as the value for opts.
 | ||||
| func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { | ||||
| 	if opts.HashFunc() != crypto.Hash(0) { | ||||
| 		return nil, errors.New("ed25519: cannot sign hashed message") | ||||
| 	} | ||||
| 
 | ||||
| 	return Sign(priv, message), nil | ||||
| } | ||||
| 
 | ||||
| // GenerateKey generates a public/private key pair using entropy from rand.
 | ||||
| // If rand is nil, crypto/rand.Reader will be used.
 | ||||
| func GenerateKey(rand io.Reader) (publicKey PublicKey, privateKey PrivateKey, err error) { | ||||
| 	if rand == nil { | ||||
| 		rand = cryptorand.Reader | ||||
| 	} | ||||
| 
 | ||||
| 	privateKey = make([]byte, PrivateKeySize) | ||||
| 	publicKey = make([]byte, PublicKeySize) | ||||
| 	_, err = io.ReadFull(rand, privateKey[:32]) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	digest := sha512.Sum512(privateKey[:32]) | ||||
| 	digest[0] &= 248 | ||||
| 	digest[31] &= 127 | ||||
| 	digest[31] |= 64 | ||||
| 
 | ||||
| 	var A edwards25519.ExtendedGroupElement | ||||
| 	var hBytes [32]byte | ||||
| 	copy(hBytes[:], digest[:]) | ||||
| 	edwards25519.GeScalarMultBase(&A, &hBytes) | ||||
| 	var publicKeyBytes [32]byte | ||||
| 	A.ToBytes(&publicKeyBytes) | ||||
| 
 | ||||
| 	copy(privateKey[32:], publicKeyBytes[:]) | ||||
| 	copy(publicKey, publicKeyBytes[:]) | ||||
| 
 | ||||
| 	return publicKey, privateKey, nil | ||||
| } | ||||
| 
 | ||||
| // Sign signs the message with privateKey and returns a signature. It will
 | ||||
| // panic if len(privateKey) is not PrivateKeySize.
 | ||||
| func Sign(privateKey PrivateKey, message []byte) []byte { | ||||
| 	if l := len(privateKey); l != PrivateKeySize { | ||||
| 		panic("ed25519: bad private key length: " + strconv.Itoa(l)) | ||||
| 	} | ||||
| 
 | ||||
| 	h := sha512.New() | ||||
| 	h.Write(privateKey[:32]) | ||||
| 
 | ||||
| 	var digest1, messageDigest, hramDigest [64]byte | ||||
| 	var expandedSecretKey [32]byte | ||||
| 	h.Sum(digest1[:0]) | ||||
| 	copy(expandedSecretKey[:], digest1[:]) | ||||
| 	expandedSecretKey[0] &= 248 | ||||
| 	expandedSecretKey[31] &= 63 | ||||
| 	expandedSecretKey[31] |= 64 | ||||
| 
 | ||||
| 	h.Reset() | ||||
| 	h.Write(digest1[32:]) | ||||
| 	h.Write(message) | ||||
| 	h.Sum(messageDigest[:0]) | ||||
| 
 | ||||
| 	var messageDigestReduced [32]byte | ||||
| 	edwards25519.ScReduce(&messageDigestReduced, &messageDigest) | ||||
| 	var R edwards25519.ExtendedGroupElement | ||||
| 	edwards25519.GeScalarMultBase(&R, &messageDigestReduced) | ||||
| 
 | ||||
| 	var encodedR [32]byte | ||||
| 	R.ToBytes(&encodedR) | ||||
| 
 | ||||
| 	h.Reset() | ||||
| 	h.Write(encodedR[:]) | ||||
| 	h.Write(privateKey[32:]) | ||||
| 	h.Write(message) | ||||
| 	h.Sum(hramDigest[:0]) | ||||
| 	var hramDigestReduced [32]byte | ||||
| 	edwards25519.ScReduce(&hramDigestReduced, &hramDigest) | ||||
| 
 | ||||
| 	var s [32]byte | ||||
| 	edwards25519.ScMulAdd(&s, &hramDigestReduced, &expandedSecretKey, &messageDigestReduced) | ||||
| 
 | ||||
| 	signature := make([]byte, SignatureSize) | ||||
| 	copy(signature[:], encodedR[:]) | ||||
| 	copy(signature[32:], s[:]) | ||||
| 
 | ||||
| 	return signature | ||||
| } | ||||
| 
 | ||||
| // Verify reports whether sig is a valid signature of message by publicKey. It
 | ||||
| // will panic if len(publicKey) is not PublicKeySize.
 | ||||
| func Verify(publicKey PublicKey, message, sig []byte) bool { | ||||
| 	if l := len(publicKey); l != PublicKeySize { | ||||
| 		panic("ed25519: bad public key length: " + strconv.Itoa(l)) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(sig) != SignatureSize || sig[63]&224 != 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	var A edwards25519.ExtendedGroupElement | ||||
| 	var publicKeyBytes [32]byte | ||||
| 	copy(publicKeyBytes[:], publicKey) | ||||
| 	if !A.FromBytes(&publicKeyBytes) { | ||||
| 		return false | ||||
| 	} | ||||
| 	edwards25519.FeNeg(&A.X, &A.X) | ||||
| 	edwards25519.FeNeg(&A.T, &A.T) | ||||
| 
 | ||||
| 	h := sha512.New() | ||||
| 	h.Write(sig[:32]) | ||||
| 	h.Write(publicKey[:]) | ||||
| 	h.Write(message) | ||||
| 	var digest [64]byte | ||||
| 	h.Sum(digest[:0]) | ||||
| 
 | ||||
| 	var hReduced [32]byte | ||||
| 	edwards25519.ScReduce(&hReduced, &digest) | ||||
| 
 | ||||
| 	var R edwards25519.ProjectiveGroupElement | ||||
| 	var b [32]byte | ||||
| 	copy(b[:], sig[32:]) | ||||
| 	edwards25519.GeDoubleScalarMultVartime(&R, &hReduced, &A, &b) | ||||
| 
 | ||||
| 	var checkR [32]byte | ||||
| 	R.ToBytes(&checkR) | ||||
| 	return bytes.Equal(sig[:32], checkR[:]) | ||||
| } | ||||
							
								
								
									
										1422
									
								
								vendor/golang.org/x/crypto/ed25519/internal/edwards25519/const.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										1422
									
								
								vendor/golang.org/x/crypto/ed25519/internal/edwards25519/const.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1771
									
								
								vendor/golang.org/x/crypto/ed25519/internal/edwards25519/edwards25519.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										1771
									
								
								vendor/golang.org/x/crypto/ed25519/internal/edwards25519/edwards25519.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,13 @@ | |||
| language: go | ||||
| 
 | ||||
| go: | ||||
|   - tip | ||||
| 
 | ||||
| install: | ||||
|   - export GOPATH="$HOME/gopath" | ||||
|   - mkdir -p "$GOPATH/src/golang.org/x" | ||||
|   - mv "$TRAVIS_BUILD_DIR" "$GOPATH/src/golang.org/x/oauth2" | ||||
|   - go get -v -t -d golang.org/x/oauth2/... | ||||
| 
 | ||||
| script: | ||||
|   - go test -v golang.org/x/oauth2/... | ||||
|  | @ -0,0 +1,3 @@ | |||
| # This source code refers to The Go Authors for copyright purposes. | ||||
| # The master list of authors is in the main Go distribution, | ||||
| # visible at http://tip.golang.org/AUTHORS. | ||||
|  | @ -0,0 +1,31 @@ | |||
| # Contributing to Go | ||||
| 
 | ||||
| Go is an open source project. | ||||
| 
 | ||||
| It is the work of hundreds of contributors. We appreciate your help! | ||||
| 
 | ||||
| 
 | ||||
| ## Filing issues | ||||
| 
 | ||||
| When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions: | ||||
| 
 | ||||
| 1. What version of Go are you using (`go version`)? | ||||
| 2. What operating system and processor architecture are you using? | ||||
| 3. What did you do? | ||||
| 4. What did you expect to see? | ||||
| 5. What did you see instead? | ||||
| 
 | ||||
| General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. | ||||
| The gophers there will answer or ask you to file an issue if you've tripped over a bug. | ||||
| 
 | ||||
| ## Contributing code | ||||
| 
 | ||||
| Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) | ||||
| before sending patches. | ||||
| 
 | ||||
| **We do not accept GitHub pull requests** | ||||
| (we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review). | ||||
| 
 | ||||
| Unless otherwise noted, the Go source files are distributed under | ||||
| the BSD-style license found in the LICENSE file. | ||||
| 
 | ||||
|  | @ -0,0 +1,3 @@ | |||
| # This source code was written by the Go contributors. | ||||
| # The master list of contributors is in the main Go distribution, | ||||
| # visible at http://tip.golang.org/CONTRIBUTORS. | ||||
|  | @ -0,0 +1,27 @@ | |||
| Copyright (c) 2009 The oauth2 Authors. All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are | ||||
| met: | ||||
| 
 | ||||
|    * Redistributions of source code must retain the above copyright | ||||
| notice, this list of conditions and the following disclaimer. | ||||
|    * Redistributions in binary form must reproduce the above | ||||
| copyright notice, this list of conditions and the following disclaimer | ||||
| in the documentation and/or other materials provided with the | ||||
| distribution. | ||||
|    * Neither the name of Google Inc. nor the names of its | ||||
| contributors may be used to endorse or promote products derived from | ||||
| this software without specific prior written permission. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | @ -0,0 +1,74 @@ | |||
| # OAuth2 for Go | ||||
| 
 | ||||
| [](https://travis-ci.org/golang/oauth2) | ||||
| [](https://godoc.org/golang.org/x/oauth2) | ||||
| 
 | ||||
| oauth2 package contains a client implementation for OAuth 2.0 spec. | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| ~~~~ | ||||
| go get golang.org/x/oauth2 | ||||
| ~~~~ | ||||
| 
 | ||||
| See godoc for further documentation and examples. | ||||
| 
 | ||||
| * [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2) | ||||
| * [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google) | ||||
| 
 | ||||
| 
 | ||||
| ## App Engine | ||||
| 
 | ||||
| In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor | ||||
| of the [`context.Context`](https://golang.org/x/net/context#Context) type from | ||||
| the `golang.org/x/net/context` package | ||||
| 
 | ||||
| This means its no longer possible to use the "Classic App Engine" | ||||
| `appengine.Context` type with the `oauth2` package. (You're using | ||||
| Classic App Engine if you import the package `"appengine"`.) | ||||
| 
 | ||||
| To work around this, you may use the new `"google.golang.org/appengine"` | ||||
| package. This package has almost the same API as the `"appengine"` package, | ||||
| but it can be fetched with `go get` and used on "Managed VMs" and well as | ||||
| Classic App Engine. | ||||
| 
 | ||||
| See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app) | ||||
| for information on updating your app. | ||||
| 
 | ||||
| If you don't want to update your entire app to use the new App Engine packages, | ||||
| you may use both sets of packages in parallel, using only the new packages | ||||
| with the `oauth2` package. | ||||
| 
 | ||||
| 	import ( | ||||
| 		"golang.org/x/net/context" | ||||
| 		"golang.org/x/oauth2" | ||||
| 		"golang.org/x/oauth2/google" | ||||
| 		newappengine "google.golang.org/appengine" | ||||
| 		newurlfetch "google.golang.org/appengine/urlfetch" | ||||
| 
 | ||||
| 		"appengine" | ||||
| 	) | ||||
| 
 | ||||
| 	func handler(w http.ResponseWriter, r *http.Request) { | ||||
| 		var c appengine.Context = appengine.NewContext(r) | ||||
| 		c.Infof("Logging a message with the old package") | ||||
| 
 | ||||
| 		var ctx context.Context = newappengine.NewContext(r) | ||||
| 		client := &http.Client{ | ||||
| 			Transport: &oauth2.Transport{ | ||||
| 				Source: google.AppEngineTokenSource(ctx, "scope"), | ||||
| 				Base:   &newurlfetch.Transport{Context: ctx}, | ||||
| 			}, | ||||
| 		} | ||||
| 		client.Get("...") | ||||
| 	} | ||||
| 
 | ||||
| ## Contributing | ||||
| 
 | ||||
| We appreciate your help! | ||||
| 
 | ||||
| To contribute, please read the contribution guidelines: | ||||
| 	https://golang.org/doc/contribute.html | ||||
| 
 | ||||
| Note that the Go project does not use GitHub pull requests but | ||||
| uses Gerrit for code reviews. See the contribution guide for details. | ||||
|  | @ -0,0 +1,25 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // +build appengine
 | ||||
| 
 | ||||
| // App Engine hooks.
 | ||||
| 
 | ||||
| package oauth2 | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"golang.org/x/net/context" | ||||
| 	"golang.org/x/oauth2/internal" | ||||
| 	"google.golang.org/appengine/urlfetch" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	internal.RegisterContextClientFunc(contextClientAppEngine) | ||||
| } | ||||
| 
 | ||||
| func contextClientAppEngine(ctx context.Context) (*http.Client, error) { | ||||
| 	return urlfetch.Client(ctx), nil | ||||
| } | ||||
|  | @ -0,0 +1,76 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // Package internal contains support packages for oauth2 package.
 | ||||
| package internal | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // ParseKey converts the binary contents of a private key file
 | ||||
| // to an *rsa.PrivateKey. It detects whether the private key is in a
 | ||||
| // PEM container or not. If so, it extracts the the private key
 | ||||
| // from PEM container before conversion. It only supports PEM
 | ||||
| // containers with no passphrase.
 | ||||
| func ParseKey(key []byte) (*rsa.PrivateKey, error) { | ||||
| 	block, _ := pem.Decode(key) | ||||
| 	if block != nil { | ||||
| 		key = block.Bytes | ||||
| 	} | ||||
| 	parsedKey, err := x509.ParsePKCS8PrivateKey(key) | ||||
| 	if err != nil { | ||||
| 		parsedKey, err = x509.ParsePKCS1PrivateKey(key) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	parsed, ok := parsedKey.(*rsa.PrivateKey) | ||||
| 	if !ok { | ||||
| 		return nil, errors.New("private key is invalid") | ||||
| 	} | ||||
| 	return parsed, nil | ||||
| } | ||||
| 
 | ||||
| func ParseINI(ini io.Reader) (map[string]map[string]string, error) { | ||||
| 	result := map[string]map[string]string{ | ||||
| 		"": {}, // root section
 | ||||
| 	} | ||||
| 	scanner := bufio.NewScanner(ini) | ||||
| 	currentSection := "" | ||||
| 	for scanner.Scan() { | ||||
| 		line := strings.TrimSpace(scanner.Text()) | ||||
| 		if strings.HasPrefix(line, ";") { | ||||
| 			// comment.
 | ||||
| 			continue | ||||
| 		} | ||||
| 		if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { | ||||
| 			currentSection = strings.TrimSpace(line[1 : len(line)-1]) | ||||
| 			result[currentSection] = map[string]string{} | ||||
| 			continue | ||||
| 		} | ||||
| 		parts := strings.SplitN(line, "=", 2) | ||||
| 		if len(parts) == 2 && parts[0] != "" { | ||||
| 			result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) | ||||
| 		} | ||||
| 	} | ||||
| 	if err := scanner.Err(); err != nil { | ||||
| 		return nil, fmt.Errorf("error scanning ini: %v", err) | ||||
| 	} | ||||
| 	return result, nil | ||||
| } | ||||
| 
 | ||||
| func CondVal(v string) []string { | ||||
| 	if v == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return []string{v} | ||||
| } | ||||
|  | @ -0,0 +1,247 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // Package internal contains support packages for oauth2 package.
 | ||||
| package internal | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"mime" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"golang.org/x/net/context" | ||||
| ) | ||||
| 
 | ||||
| // Token represents the crendentials used to authorize
 | ||||
| // the requests to access protected resources on the OAuth 2.0
 | ||||
| // provider's backend.
 | ||||
| //
 | ||||
| // This type is a mirror of oauth2.Token and exists to break
 | ||||
| // an otherwise-circular dependency. Other internal packages
 | ||||
| // should convert this Token into an oauth2.Token before use.
 | ||||
| type Token struct { | ||||
| 	// AccessToken is the token that authorizes and authenticates
 | ||||
| 	// the requests.
 | ||||
| 	AccessToken string | ||||
| 
 | ||||
| 	// TokenType is the type of token.
 | ||||
| 	// The Type method returns either this or "Bearer", the default.
 | ||||
| 	TokenType string | ||||
| 
 | ||||
| 	// RefreshToken is a token that's used by the application
 | ||||
| 	// (as opposed to the user) to refresh the access token
 | ||||
| 	// if it expires.
 | ||||
| 	RefreshToken string | ||||
| 
 | ||||
| 	// Expiry is the optional expiration time of the access token.
 | ||||
| 	//
 | ||||
| 	// If zero, TokenSource implementations will reuse the same
 | ||||
| 	// token forever and RefreshToken or equivalent
 | ||||
| 	// mechanisms for that TokenSource will not be used.
 | ||||
| 	Expiry time.Time | ||||
| 
 | ||||
| 	// Raw optionally contains extra metadata from the server
 | ||||
| 	// when updating a token.
 | ||||
| 	Raw interface{} | ||||
| } | ||||
| 
 | ||||
| // tokenJSON is the struct representing the HTTP response from OAuth2
 | ||||
| // providers returning a token in JSON form.
 | ||||
| type tokenJSON struct { | ||||
| 	AccessToken  string         `json:"access_token"` | ||||
| 	TokenType    string         `json:"token_type"` | ||||
| 	RefreshToken string         `json:"refresh_token"` | ||||
| 	ExpiresIn    expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
 | ||||
| 	Expires      expirationTime `json:"expires"`    // broken Facebook spelling of expires_in
 | ||||
| } | ||||
| 
 | ||||
| func (e *tokenJSON) expiry() (t time.Time) { | ||||
| 	if v := e.ExpiresIn; v != 0 { | ||||
| 		return time.Now().Add(time.Duration(v) * time.Second) | ||||
| 	} | ||||
| 	if v := e.Expires; v != 0 { | ||||
| 		return time.Now().Add(time.Duration(v) * time.Second) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| type expirationTime int32 | ||||
| 
 | ||||
| func (e *expirationTime) UnmarshalJSON(b []byte) error { | ||||
| 	var n json.Number | ||||
| 	err := json.Unmarshal(b, &n) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	i, err := n.Int64() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*e = expirationTime(i) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var brokenAuthHeaderProviders = []string{ | ||||
| 	"https://accounts.google.com/", | ||||
| 	"https://api.codeswholesale.com/oauth/token", | ||||
| 	"https://api.dropbox.com/", | ||||
| 	"https://api.dropboxapi.com/", | ||||
| 	"https://api.instagram.com/", | ||||
| 	"https://api.netatmo.net/", | ||||
| 	"https://api.odnoklassniki.ru/", | ||||
| 	"https://api.pushbullet.com/", | ||||
| 	"https://api.soundcloud.com/", | ||||
| 	"https://api.twitch.tv/", | ||||
| 	"https://app.box.com/", | ||||
| 	"https://connect.stripe.com/", | ||||
| 	"https://graph.facebook.com", // see https://github.com/golang/oauth2/issues/214
 | ||||
| 	"https://login.microsoftonline.com/", | ||||
| 	"https://login.salesforce.com/", | ||||
| 	"https://oauth.sandbox.trainingpeaks.com/", | ||||
| 	"https://oauth.trainingpeaks.com/", | ||||
| 	"https://oauth.vk.com/", | ||||
| 	"https://openapi.baidu.com/", | ||||
| 	"https://slack.com/", | ||||
| 	"https://test-sandbox.auth.corp.google.com", | ||||
| 	"https://test.salesforce.com/", | ||||
| 	"https://user.gini.net/", | ||||
| 	"https://www.douban.com/", | ||||
| 	"https://www.googleapis.com/", | ||||
| 	"https://www.linkedin.com/", | ||||
| 	"https://www.strava.com/oauth/", | ||||
| 	"https://www.wunderlist.com/oauth/", | ||||
| 	"https://api.patreon.com/", | ||||
| 	"https://sandbox.codeswholesale.com/oauth/token", | ||||
| } | ||||
| 
 | ||||
| // brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
 | ||||
| var brokenAuthHeaderDomains = []string{ | ||||
| 	".force.com", | ||||
| 	".okta.com", | ||||
| 	".oktapreview.com", | ||||
| } | ||||
| 
 | ||||
| func RegisterBrokenAuthHeaderProvider(tokenURL string) { | ||||
| 	brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL) | ||||
| } | ||||
| 
 | ||||
| // providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
 | ||||
| // implements the OAuth2 spec correctly
 | ||||
| // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
 | ||||
| // In summary:
 | ||||
| // - Reddit only accepts client secret in the Authorization header
 | ||||
| // - Dropbox accepts either it in URL param or Auth header, but not both.
 | ||||
| // - Google only accepts URL param (not spec compliant?), not Auth header
 | ||||
| // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
 | ||||
| func providerAuthHeaderWorks(tokenURL string) bool { | ||||
| 	for _, s := range brokenAuthHeaderProviders { | ||||
| 		if strings.HasPrefix(tokenURL, s) { | ||||
| 			// Some sites fail to implement the OAuth2 spec fully.
 | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if u, err := url.Parse(tokenURL); err == nil { | ||||
| 		for _, s := range brokenAuthHeaderDomains { | ||||
| 			if strings.HasSuffix(u.Host, s) { | ||||
| 				return false | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Assume the provider implements the spec properly
 | ||||
| 	// otherwise. We can add more exceptions as they're
 | ||||
| 	// discovered. We will _not_ be adding configurable hooks
 | ||||
| 	// to this package to let users select server bugs.
 | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) { | ||||
| 	hc, err := ContextClient(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	bustedAuth := !providerAuthHeaderWorks(tokenURL) | ||||
| 	if bustedAuth { | ||||
| 		if clientID != "" { | ||||
| 			v.Set("client_id", clientID) | ||||
| 		} | ||||
| 		if clientSecret != "" { | ||||
| 			v.Set("client_secret", clientSecret) | ||||
| 		} | ||||
| 	} | ||||
| 	req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode())) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||||
| 	if !bustedAuth { | ||||
| 		req.SetBasicAuth(clientID, clientSecret) | ||||
| 	} | ||||
| 	r, err := hc.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer r.Body.Close() | ||||
| 	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) | ||||
| 	} | ||||
| 	if code := r.StatusCode; code < 200 || code > 299 { | ||||
| 		return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body) | ||||
| 	} | ||||
| 
 | ||||
| 	var token *Token | ||||
| 	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) | ||||
| 	switch content { | ||||
| 	case "application/x-www-form-urlencoded", "text/plain": | ||||
| 		vals, err := url.ParseQuery(string(body)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		token = &Token{ | ||||
| 			AccessToken:  vals.Get("access_token"), | ||||
| 			TokenType:    vals.Get("token_type"), | ||||
| 			RefreshToken: vals.Get("refresh_token"), | ||||
| 			Raw:          vals, | ||||
| 		} | ||||
| 		e := vals.Get("expires_in") | ||||
| 		if e == "" { | ||||
| 			// TODO(jbd): Facebook's OAuth2 implementation is broken and
 | ||||
| 			// returns expires_in field in expires. Remove the fallback to expires,
 | ||||
| 			// when Facebook fixes their implementation.
 | ||||
| 			e = vals.Get("expires") | ||||
| 		} | ||||
| 		expires, _ := strconv.Atoi(e) | ||||
| 		if expires != 0 { | ||||
| 			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) | ||||
| 		} | ||||
| 	default: | ||||
| 		var tj tokenJSON | ||||
| 		if err = json.Unmarshal(body, &tj); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		token = &Token{ | ||||
| 			AccessToken:  tj.AccessToken, | ||||
| 			TokenType:    tj.TokenType, | ||||
| 			RefreshToken: tj.RefreshToken, | ||||
| 			Expiry:       tj.expiry(), | ||||
| 			Raw:          make(map[string]interface{}), | ||||
| 		} | ||||
| 		json.Unmarshal(body, &token.Raw) // no error checks for optional fields
 | ||||
| 	} | ||||
| 	// Don't overwrite `RefreshToken` with an empty value
 | ||||
| 	// if this was a token refreshing request.
 | ||||
| 	if token.RefreshToken == "" { | ||||
| 		token.RefreshToken = v.Get("refresh_token") | ||||
| 	} | ||||
| 	return token, nil | ||||
| } | ||||
|  | @ -0,0 +1,69 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // Package internal contains support packages for oauth2 package.
 | ||||
| package internal | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"golang.org/x/net/context" | ||||
| ) | ||||
| 
 | ||||
| // HTTPClient is the context key to use with golang.org/x/net/context's
 | ||||
| // WithValue function to associate an *http.Client value with a context.
 | ||||
| var HTTPClient ContextKey | ||||
| 
 | ||||
| // ContextKey is just an empty struct. It exists so HTTPClient can be
 | ||||
| // an immutable public variable with a unique type. It's immutable
 | ||||
| // because nobody else can create a ContextKey, being unexported.
 | ||||
| type ContextKey struct{} | ||||
| 
 | ||||
| // ContextClientFunc is a func which tries to return an *http.Client
 | ||||
| // given a Context value. If it returns an error, the search stops
 | ||||
| // with that error.  If it returns (nil, nil), the search continues
 | ||||
| // down the list of registered funcs.
 | ||||
| type ContextClientFunc func(context.Context) (*http.Client, error) | ||||
| 
 | ||||
| var contextClientFuncs []ContextClientFunc | ||||
| 
 | ||||
| func RegisterContextClientFunc(fn ContextClientFunc) { | ||||
| 	contextClientFuncs = append(contextClientFuncs, fn) | ||||
| } | ||||
| 
 | ||||
| func ContextClient(ctx context.Context) (*http.Client, error) { | ||||
| 	if ctx != nil { | ||||
| 		if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { | ||||
| 			return hc, nil | ||||
| 		} | ||||
| 	} | ||||
| 	for _, fn := range contextClientFuncs { | ||||
| 		c, err := fn(ctx) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if c != nil { | ||||
| 			return c, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return http.DefaultClient, nil | ||||
| } | ||||
| 
 | ||||
| func ContextTransport(ctx context.Context) http.RoundTripper { | ||||
| 	hc, err := ContextClient(ctx) | ||||
| 	// This is a rare error case (somebody using nil on App Engine).
 | ||||
| 	if err != nil { | ||||
| 		return ErrorTransport{err} | ||||
| 	} | ||||
| 	return hc.Transport | ||||
| } | ||||
| 
 | ||||
| // ErrorTransport returns the specified error on RoundTrip.
 | ||||
| // This RoundTripper should be used in rare error cases where
 | ||||
| // error handling can be postponed to response handling time.
 | ||||
| type ErrorTransport struct{ Err error } | ||||
| 
 | ||||
| func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) { | ||||
| 	return nil, t.Err | ||||
| } | ||||
|  | @ -0,0 +1,340 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // Package oauth2 provides support for making
 | ||||
| // OAuth2 authorized and authenticated HTTP requests.
 | ||||
| // It can additionally grant authorization with Bearer JWT.
 | ||||
| package oauth2 | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"golang.org/x/net/context" | ||||
| 	"golang.org/x/oauth2/internal" | ||||
| ) | ||||
| 
 | ||||
| // NoContext is the default context you should supply if not using
 | ||||
| // your own context.Context (see https://golang.org/x/net/context).
 | ||||
| //
 | ||||
| // Deprecated: Use context.Background() or context.TODO() instead.
 | ||||
| var NoContext = context.TODO() | ||||
| 
 | ||||
| // RegisterBrokenAuthHeaderProvider registers an OAuth2 server
 | ||||
| // identified by the tokenURL prefix as an OAuth2 implementation
 | ||||
| // which doesn't support the HTTP Basic authentication
 | ||||
| // scheme to authenticate with the authorization server.
 | ||||
| // Once a server is registered, credentials (client_id and client_secret)
 | ||||
| // will be passed as query parameters rather than being present
 | ||||
| // in the Authorization header.
 | ||||
| // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
 | ||||
| func RegisterBrokenAuthHeaderProvider(tokenURL string) { | ||||
| 	internal.RegisterBrokenAuthHeaderProvider(tokenURL) | ||||
| } | ||||
| 
 | ||||
| // Config describes a typical 3-legged OAuth2 flow, with both the
 | ||||
| // client application information and the server's endpoint URLs.
 | ||||
| // For the client credentials 2-legged OAuth2 flow, see the clientcredentials
 | ||||
| // package (https://golang.org/x/oauth2/clientcredentials).
 | ||||
| type Config struct { | ||||
| 	// ClientID is the application's ID.
 | ||||
| 	ClientID string | ||||
| 
 | ||||
| 	// ClientSecret is the application's secret.
 | ||||
| 	ClientSecret string | ||||
| 
 | ||||
| 	// Endpoint contains the resource server's token endpoint
 | ||||
| 	// URLs. These are constants specific to each server and are
 | ||||
| 	// often available via site-specific packages, such as
 | ||||
| 	// google.Endpoint or github.Endpoint.
 | ||||
| 	Endpoint Endpoint | ||||
| 
 | ||||
| 	// RedirectURL is the URL to redirect users going through
 | ||||
| 	// the OAuth flow, after the resource owner's URLs.
 | ||||
| 	RedirectURL string | ||||
| 
 | ||||
| 	// Scope specifies optional requested permissions.
 | ||||
| 	Scopes []string | ||||
| } | ||||
| 
 | ||||
| // A TokenSource is anything that can return a token.
 | ||||
| type TokenSource interface { | ||||
| 	// Token returns a token or an error.
 | ||||
| 	// Token must be safe for concurrent use by multiple goroutines.
 | ||||
| 	// The returned Token must not be modified.
 | ||||
| 	Token() (*Token, error) | ||||
| } | ||||
| 
 | ||||
| // Endpoint contains the OAuth 2.0 provider's authorization and token
 | ||||
| // endpoint URLs.
 | ||||
| type Endpoint struct { | ||||
| 	AuthURL  string | ||||
| 	TokenURL string | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	// AccessTypeOnline and AccessTypeOffline are options passed
 | ||||
| 	// to the Options.AuthCodeURL method. They modify the
 | ||||
| 	// "access_type" field that gets sent in the URL returned by
 | ||||
| 	// AuthCodeURL.
 | ||||
| 	//
 | ||||
| 	// Online is the default if neither is specified. If your
 | ||||
| 	// application needs to refresh access tokens when the user
 | ||||
| 	// is not present at the browser, then use offline. This will
 | ||||
| 	// result in your application obtaining a refresh token the
 | ||||
| 	// first time your application exchanges an authorization
 | ||||
| 	// code for a user.
 | ||||
| 	AccessTypeOnline  AuthCodeOption = SetAuthURLParam("access_type", "online") | ||||
| 	AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline") | ||||
| 
 | ||||
| 	// ApprovalForce forces the users to view the consent dialog
 | ||||
| 	// and confirm the permissions request at the URL returned
 | ||||
| 	// from AuthCodeURL, even if they've already done so.
 | ||||
| 	ApprovalForce AuthCodeOption = SetAuthURLParam("approval_prompt", "force") | ||||
| ) | ||||
| 
 | ||||
| // An AuthCodeOption is passed to Config.AuthCodeURL.
 | ||||
| type AuthCodeOption interface { | ||||
| 	setValue(url.Values) | ||||
| } | ||||
| 
 | ||||
| type setParam struct{ k, v string } | ||||
| 
 | ||||
| func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) } | ||||
| 
 | ||||
| // SetAuthURLParam builds an AuthCodeOption which passes key/value parameters
 | ||||
| // to a provider's authorization endpoint.
 | ||||
| func SetAuthURLParam(key, value string) AuthCodeOption { | ||||
| 	return setParam{key, value} | ||||
| } | ||||
| 
 | ||||
| // AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
 | ||||
| // that asks for permissions for the required scopes explicitly.
 | ||||
| //
 | ||||
| // State is a token to protect the user from CSRF attacks. You must
 | ||||
| // always provide a non-zero string and validate that it matches the
 | ||||
| // the state query parameter on your redirect callback.
 | ||||
| // See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
 | ||||
| //
 | ||||
| // Opts may include AccessTypeOnline or AccessTypeOffline, as well
 | ||||
| // as ApprovalForce.
 | ||||
| func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { | ||||
| 	var buf bytes.Buffer | ||||
| 	buf.WriteString(c.Endpoint.AuthURL) | ||||
| 	v := url.Values{ | ||||
| 		"response_type": {"code"}, | ||||
| 		"client_id":     {c.ClientID}, | ||||
| 		"redirect_uri":  internal.CondVal(c.RedirectURL), | ||||
| 		"scope":         internal.CondVal(strings.Join(c.Scopes, " ")), | ||||
| 		"state":         internal.CondVal(state), | ||||
| 	} | ||||
| 	for _, opt := range opts { | ||||
| 		opt.setValue(v) | ||||
| 	} | ||||
| 	if strings.Contains(c.Endpoint.AuthURL, "?") { | ||||
| 		buf.WriteByte('&') | ||||
| 	} else { | ||||
| 		buf.WriteByte('?') | ||||
| 	} | ||||
| 	buf.WriteString(v.Encode()) | ||||
| 	return buf.String() | ||||
| } | ||||
| 
 | ||||
| // PasswordCredentialsToken converts a resource owner username and password
 | ||||
| // pair into a token.
 | ||||
| //
 | ||||
| // Per the RFC, this grant type should only be used "when there is a high
 | ||||
| // degree of trust between the resource owner and the client (e.g., the client
 | ||||
| // is part of the device operating system or a highly privileged application),
 | ||||
| // and when other authorization grant types are not available."
 | ||||
| // See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.
 | ||||
| //
 | ||||
| // The HTTP client to use is derived from the context.
 | ||||
| // If nil, http.DefaultClient is used.
 | ||||
| func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) { | ||||
| 	return retrieveToken(ctx, c, url.Values{ | ||||
| 		"grant_type": {"password"}, | ||||
| 		"username":   {username}, | ||||
| 		"password":   {password}, | ||||
| 		"scope":      internal.CondVal(strings.Join(c.Scopes, " ")), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Exchange converts an authorization code into a token.
 | ||||
| //
 | ||||
| // It is used after a resource provider redirects the user back
 | ||||
| // to the Redirect URI (the URL obtained from AuthCodeURL).
 | ||||
| //
 | ||||
| // The HTTP client to use is derived from the context.
 | ||||
| // If a client is not provided via the context, http.DefaultClient is used.
 | ||||
| //
 | ||||
| // The code will be in the *http.Request.FormValue("code"). Before
 | ||||
| // calling Exchange, be sure to validate FormValue("state").
 | ||||
| func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { | ||||
| 	return retrieveToken(ctx, c, url.Values{ | ||||
| 		"grant_type":   {"authorization_code"}, | ||||
| 		"code":         {code}, | ||||
| 		"redirect_uri": internal.CondVal(c.RedirectURL), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Client returns an HTTP client using the provided token.
 | ||||
| // The token will auto-refresh as necessary. The underlying
 | ||||
| // HTTP transport will be obtained using the provided context.
 | ||||
| // The returned client and its Transport should not be modified.
 | ||||
| func (c *Config) Client(ctx context.Context, t *Token) *http.Client { | ||||
| 	return NewClient(ctx, c.TokenSource(ctx, t)) | ||||
| } | ||||
| 
 | ||||
| // TokenSource returns a TokenSource that returns t until t expires,
 | ||||
| // automatically refreshing it as necessary using the provided context.
 | ||||
| //
 | ||||
| // Most users will use Config.Client instead.
 | ||||
| func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource { | ||||
| 	tkr := &tokenRefresher{ | ||||
| 		ctx:  ctx, | ||||
| 		conf: c, | ||||
| 	} | ||||
| 	if t != nil { | ||||
| 		tkr.refreshToken = t.RefreshToken | ||||
| 	} | ||||
| 	return &reuseTokenSource{ | ||||
| 		t:   t, | ||||
| 		new: tkr, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token"
 | ||||
| // HTTP requests to renew a token using a RefreshToken.
 | ||||
| type tokenRefresher struct { | ||||
| 	ctx          context.Context // used to get HTTP requests
 | ||||
| 	conf         *Config | ||||
| 	refreshToken string | ||||
| } | ||||
| 
 | ||||
| // WARNING: Token is not safe for concurrent access, as it
 | ||||
| // updates the tokenRefresher's refreshToken field.
 | ||||
| // Within this package, it is used by reuseTokenSource which
 | ||||
| // synchronizes calls to this method with its own mutex.
 | ||||
| func (tf *tokenRefresher) Token() (*Token, error) { | ||||
| 	if tf.refreshToken == "" { | ||||
| 		return nil, errors.New("oauth2: token expired and refresh token is not set") | ||||
| 	} | ||||
| 
 | ||||
| 	tk, err := retrieveToken(tf.ctx, tf.conf, url.Values{ | ||||
| 		"grant_type":    {"refresh_token"}, | ||||
| 		"refresh_token": {tf.refreshToken}, | ||||
| 	}) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if tf.refreshToken != tk.RefreshToken { | ||||
| 		tf.refreshToken = tk.RefreshToken | ||||
| 	} | ||||
| 	return tk, err | ||||
| } | ||||
| 
 | ||||
| // reuseTokenSource is a TokenSource that holds a single token in memory
 | ||||
| // and validates its expiry before each call to retrieve it with
 | ||||
| // Token. If it's expired, it will be auto-refreshed using the
 | ||||
| // new TokenSource.
 | ||||
| type reuseTokenSource struct { | ||||
| 	new TokenSource // called when t is expired.
 | ||||
| 
 | ||||
| 	mu sync.Mutex // guards t
 | ||||
| 	t  *Token | ||||
| } | ||||
| 
 | ||||
| // Token returns the current token if it's still valid, else will
 | ||||
| // refresh the current token (using r.Context for HTTP client
 | ||||
| // information) and return the new one.
 | ||||
| func (s *reuseTokenSource) Token() (*Token, error) { | ||||
| 	s.mu.Lock() | ||||
| 	defer s.mu.Unlock() | ||||
| 	if s.t.Valid() { | ||||
| 		return s.t, nil | ||||
| 	} | ||||
| 	t, err := s.new.Token() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	s.t = t | ||||
| 	return t, nil | ||||
| } | ||||
| 
 | ||||
| // StaticTokenSource returns a TokenSource that always returns the same token.
 | ||||
| // Because the provided token t is never refreshed, StaticTokenSource is only
 | ||||
| // useful for tokens that never expire.
 | ||||
| func StaticTokenSource(t *Token) TokenSource { | ||||
| 	return staticTokenSource{t} | ||||
| } | ||||
| 
 | ||||
| // staticTokenSource is a TokenSource that always returns the same Token.
 | ||||
| type staticTokenSource struct { | ||||
| 	t *Token | ||||
| } | ||||
| 
 | ||||
| func (s staticTokenSource) Token() (*Token, error) { | ||||
| 	return s.t, nil | ||||
| } | ||||
| 
 | ||||
| // HTTPClient is the context key to use with golang.org/x/net/context's
 | ||||
| // WithValue function to associate an *http.Client value with a context.
 | ||||
| var HTTPClient internal.ContextKey | ||||
| 
 | ||||
| // NewClient creates an *http.Client from a Context and TokenSource.
 | ||||
| // The returned client is not valid beyond the lifetime of the context.
 | ||||
| //
 | ||||
| // As a special case, if src is nil, a non-OAuth2 client is returned
 | ||||
| // using the provided context. This exists to support related OAuth2
 | ||||
| // packages.
 | ||||
| func NewClient(ctx context.Context, src TokenSource) *http.Client { | ||||
| 	if src == nil { | ||||
| 		c, err := internal.ContextClient(ctx) | ||||
| 		if err != nil { | ||||
| 			return &http.Client{Transport: internal.ErrorTransport{Err: err}} | ||||
| 		} | ||||
| 		return c | ||||
| 	} | ||||
| 	return &http.Client{ | ||||
| 		Transport: &Transport{ | ||||
| 			Base:   internal.ContextTransport(ctx), | ||||
| 			Source: ReuseTokenSource(nil, src), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ReuseTokenSource returns a TokenSource which repeatedly returns the
 | ||||
| // same token as long as it's valid, starting with t.
 | ||||
| // When its cached token is invalid, a new token is obtained from src.
 | ||||
| //
 | ||||
| // ReuseTokenSource is typically used to reuse tokens from a cache
 | ||||
| // (such as a file on disk) between runs of a program, rather than
 | ||||
| // obtaining new tokens unnecessarily.
 | ||||
| //
 | ||||
| // The initial token t may be nil, in which case the TokenSource is
 | ||||
| // wrapped in a caching version if it isn't one already. This also
 | ||||
| // means it's always safe to wrap ReuseTokenSource around any other
 | ||||
| // TokenSource without adverse effects.
 | ||||
| func ReuseTokenSource(t *Token, src TokenSource) TokenSource { | ||||
| 	// Don't wrap a reuseTokenSource in itself. That would work,
 | ||||
| 	// but cause an unnecessary number of mutex operations.
 | ||||
| 	// Just build the equivalent one.
 | ||||
| 	if rt, ok := src.(*reuseTokenSource); ok { | ||||
| 		if t == nil { | ||||
| 			// Just use it directly.
 | ||||
| 			return rt | ||||
| 		} | ||||
| 		src = rt.new | ||||
| 	} | ||||
| 	return &reuseTokenSource{ | ||||
| 		t:   t, | ||||
| 		new: src, | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,158 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package oauth2 | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"golang.org/x/net/context" | ||||
| 	"golang.org/x/oauth2/internal" | ||||
| ) | ||||
| 
 | ||||
| // expiryDelta determines how earlier a token should be considered
 | ||||
| // expired than its actual expiration time. It is used to avoid late
 | ||||
| // expirations due to client-server time mismatches.
 | ||||
| const expiryDelta = 10 * time.Second | ||||
| 
 | ||||
| // Token represents the crendentials used to authorize
 | ||||
| // the requests to access protected resources on the OAuth 2.0
 | ||||
| // provider's backend.
 | ||||
| //
 | ||||
| // Most users of this package should not access fields of Token
 | ||||
| // directly. They're exported mostly for use by related packages
 | ||||
| // implementing derivative OAuth2 flows.
 | ||||
| type Token struct { | ||||
| 	// AccessToken is the token that authorizes and authenticates
 | ||||
| 	// the requests.
 | ||||
| 	AccessToken string `json:"access_token"` | ||||
| 
 | ||||
| 	// TokenType is the type of token.
 | ||||
| 	// The Type method returns either this or "Bearer", the default.
 | ||||
| 	TokenType string `json:"token_type,omitempty"` | ||||
| 
 | ||||
| 	// RefreshToken is a token that's used by the application
 | ||||
| 	// (as opposed to the user) to refresh the access token
 | ||||
| 	// if it expires.
 | ||||
| 	RefreshToken string `json:"refresh_token,omitempty"` | ||||
| 
 | ||||
| 	// Expiry is the optional expiration time of the access token.
 | ||||
| 	//
 | ||||
| 	// If zero, TokenSource implementations will reuse the same
 | ||||
| 	// token forever and RefreshToken or equivalent
 | ||||
| 	// mechanisms for that TokenSource will not be used.
 | ||||
| 	Expiry time.Time `json:"expiry,omitempty"` | ||||
| 
 | ||||
| 	// raw optionally contains extra metadata from the server
 | ||||
| 	// when updating a token.
 | ||||
| 	raw interface{} | ||||
| } | ||||
| 
 | ||||
| // Type returns t.TokenType if non-empty, else "Bearer".
 | ||||
| func (t *Token) Type() string { | ||||
| 	if strings.EqualFold(t.TokenType, "bearer") { | ||||
| 		return "Bearer" | ||||
| 	} | ||||
| 	if strings.EqualFold(t.TokenType, "mac") { | ||||
| 		return "MAC" | ||||
| 	} | ||||
| 	if strings.EqualFold(t.TokenType, "basic") { | ||||
| 		return "Basic" | ||||
| 	} | ||||
| 	if t.TokenType != "" { | ||||
| 		return t.TokenType | ||||
| 	} | ||||
| 	return "Bearer" | ||||
| } | ||||
| 
 | ||||
| // SetAuthHeader sets the Authorization header to r using the access
 | ||||
| // token in t.
 | ||||
| //
 | ||||
| // This method is unnecessary when using Transport or an HTTP Client
 | ||||
| // returned by this package.
 | ||||
| func (t *Token) SetAuthHeader(r *http.Request) { | ||||
| 	r.Header.Set("Authorization", t.Type()+" "+t.AccessToken) | ||||
| } | ||||
| 
 | ||||
| // WithExtra returns a new Token that's a clone of t, but using the
 | ||||
| // provided raw extra map. This is only intended for use by packages
 | ||||
| // implementing derivative OAuth2 flows.
 | ||||
| func (t *Token) WithExtra(extra interface{}) *Token { | ||||
| 	t2 := new(Token) | ||||
| 	*t2 = *t | ||||
| 	t2.raw = extra | ||||
| 	return t2 | ||||
| } | ||||
| 
 | ||||
| // Extra returns an extra field.
 | ||||
| // Extra fields are key-value pairs returned by the server as a
 | ||||
| // part of the token retrieval response.
 | ||||
| func (t *Token) Extra(key string) interface{} { | ||||
| 	if raw, ok := t.raw.(map[string]interface{}); ok { | ||||
| 		return raw[key] | ||||
| 	} | ||||
| 
 | ||||
| 	vals, ok := t.raw.(url.Values) | ||||
| 	if !ok { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	v := vals.Get(key) | ||||
| 	switch s := strings.TrimSpace(v); strings.Count(s, ".") { | ||||
| 	case 0: // Contains no "."; try to parse as int
 | ||||
| 		if i, err := strconv.ParseInt(s, 10, 64); err == nil { | ||||
| 			return i | ||||
| 		} | ||||
| 	case 1: // Contains a single "."; try to parse as float
 | ||||
| 		if f, err := strconv.ParseFloat(s, 64); err == nil { | ||||
| 			return f | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return v | ||||
| } | ||||
| 
 | ||||
| // expired reports whether the token is expired.
 | ||||
| // t must be non-nil.
 | ||||
| func (t *Token) expired() bool { | ||||
| 	if t.Expiry.IsZero() { | ||||
| 		return false | ||||
| 	} | ||||
| 	return t.Expiry.Add(-expiryDelta).Before(time.Now()) | ||||
| } | ||||
| 
 | ||||
| // Valid reports whether t is non-nil, has an AccessToken, and is not expired.
 | ||||
| func (t *Token) Valid() bool { | ||||
| 	return t != nil && t.AccessToken != "" && !t.expired() | ||||
| } | ||||
| 
 | ||||
| // tokenFromInternal maps an *internal.Token struct into
 | ||||
| // a *Token struct.
 | ||||
| func tokenFromInternal(t *internal.Token) *Token { | ||||
| 	if t == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return &Token{ | ||||
| 		AccessToken:  t.AccessToken, | ||||
| 		TokenType:    t.TokenType, | ||||
| 		RefreshToken: t.RefreshToken, | ||||
| 		Expiry:       t.Expiry, | ||||
| 		raw:          t.Raw, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
 | ||||
| // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
 | ||||
| // with an error..
 | ||||
| func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { | ||||
| 	tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return tokenFromInternal(tk), nil | ||||
| } | ||||
|  | @ -0,0 +1,132 @@ | |||
| // Copyright 2014 The Go Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a BSD-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package oauth2 | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| // Transport is an http.RoundTripper that makes OAuth 2.0 HTTP requests,
 | ||||
| // wrapping a base RoundTripper and adding an Authorization header
 | ||||
| // with a token from the supplied Sources.
 | ||||
| //
 | ||||
| // Transport is a low-level mechanism. Most code will use the
 | ||||
| // higher-level Config.Client method instead.
 | ||||
| type Transport struct { | ||||
| 	// Source supplies the token to add to outgoing requests'
 | ||||
| 	// Authorization headers.
 | ||||
| 	Source TokenSource | ||||
| 
 | ||||
| 	// Base is the base RoundTripper used to make HTTP requests.
 | ||||
| 	// If nil, http.DefaultTransport is used.
 | ||||
| 	Base http.RoundTripper | ||||
| 
 | ||||
| 	mu     sync.Mutex                      // guards modReq
 | ||||
| 	modReq map[*http.Request]*http.Request // original -> modified
 | ||||
| } | ||||
| 
 | ||||
| // RoundTrip authorizes and authenticates the request with an
 | ||||
| // access token. If no token exists or token is expired,
 | ||||
| // tries to refresh/fetch a new token.
 | ||||
| func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { | ||||
| 	if t.Source == nil { | ||||
| 		return nil, errors.New("oauth2: Transport's Source is nil") | ||||
| 	} | ||||
| 	token, err := t.Source.Token() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req2 := cloneRequest(req) // per RoundTripper contract
 | ||||
| 	token.SetAuthHeader(req2) | ||||
| 	t.setModReq(req, req2) | ||||
| 	res, err := t.base().RoundTrip(req2) | ||||
| 	if err != nil { | ||||
| 		t.setModReq(req, nil) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	res.Body = &onEOFReader{ | ||||
| 		rc: res.Body, | ||||
| 		fn: func() { t.setModReq(req, nil) }, | ||||
| 	} | ||||
| 	return res, nil | ||||
| } | ||||
| 
 | ||||
| // CancelRequest cancels an in-flight request by closing its connection.
 | ||||
| func (t *Transport) CancelRequest(req *http.Request) { | ||||
| 	type canceler interface { | ||||
| 		CancelRequest(*http.Request) | ||||
| 	} | ||||
| 	if cr, ok := t.base().(canceler); ok { | ||||
| 		t.mu.Lock() | ||||
| 		modReq := t.modReq[req] | ||||
| 		delete(t.modReq, req) | ||||
| 		t.mu.Unlock() | ||||
| 		cr.CancelRequest(modReq) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (t *Transport) base() http.RoundTripper { | ||||
| 	if t.Base != nil { | ||||
| 		return t.Base | ||||
| 	} | ||||
| 	return http.DefaultTransport | ||||
| } | ||||
| 
 | ||||
| func (t *Transport) setModReq(orig, mod *http.Request) { | ||||
| 	t.mu.Lock() | ||||
| 	defer t.mu.Unlock() | ||||
| 	if t.modReq == nil { | ||||
| 		t.modReq = make(map[*http.Request]*http.Request) | ||||
| 	} | ||||
| 	if mod == nil { | ||||
| 		delete(t.modReq, orig) | ||||
| 	} else { | ||||
| 		t.modReq[orig] = mod | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // cloneRequest returns a clone of the provided *http.Request.
 | ||||
| // The clone is a shallow copy of the struct and its Header map.
 | ||||
| func cloneRequest(r *http.Request) *http.Request { | ||||
| 	// shallow copy of the struct
 | ||||
| 	r2 := new(http.Request) | ||||
| 	*r2 = *r | ||||
| 	// deep copy of the Header
 | ||||
| 	r2.Header = make(http.Header, len(r.Header)) | ||||
| 	for k, s := range r.Header { | ||||
| 		r2.Header[k] = append([]string(nil), s...) | ||||
| 	} | ||||
| 	return r2 | ||||
| } | ||||
| 
 | ||||
| type onEOFReader struct { | ||||
| 	rc io.ReadCloser | ||||
| 	fn func() | ||||
| } | ||||
| 
 | ||||
| func (r *onEOFReader) Read(p []byte) (n int, err error) { | ||||
| 	n, err = r.rc.Read(p) | ||||
| 	if err == io.EOF { | ||||
| 		r.runFunc() | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (r *onEOFReader) Close() error { | ||||
| 	err := r.rc.Close() | ||||
| 	r.runFunc() | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (r *onEOFReader) runFunc() { | ||||
| 	if fn := r.fn; fn != nil { | ||||
| 		fn() | ||||
| 		r.fn = nil | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| '|Ę&{tÄU|gGę(ěŹCy=+¨śňcű:u:/pś#~žü["±4¤!nŮAŞDK<Šuf˙hĹażÂ:şü¸ˇ´B/ŁŘ¤ą¤ň_<C588>hÎŰSăT*wĚxĽŻťą-ç|ťŕŔÓ<C594>ŃÄäóĚ㣗A$$â6ŁÁâG)8nĎpűĆˡ3ĚšśoďĎvŽB–3ż]xÝ“Ó2l§G•|qRŢŻ
ö2
5R–Ó×Ç$´ń˝YčˇŢÝ™l‘Ë«yAI"ŰŚ<C5B0>®íĂ»ąĽkÄ|Kĺţ[9ĆâŇĺ=°ú˙źń|@S•3ó#ćťx?ľV„,ľ‚SĆÝőśwPíogŇ6&V6	©D.dBŠ7 | ||||
|  | @ -0,0 +1,7 @@ | |||
| *~ | ||||
| .*.swp | ||||
| *.out | ||||
| *.test | ||||
| *.pem | ||||
| *.cov | ||||
| jose-util/jose-util | ||||
|  | @ -0,0 +1,47 @@ | |||
| language: go | ||||
| 
 | ||||
| sudo: false | ||||
| 
 | ||||
| matrix: | ||||
|   fast_finish: true | ||||
|   allow_failures: | ||||
|     - go: tip | ||||
| 
 | ||||
| go: | ||||
| - 1.5 | ||||
| - 1.6 | ||||
| - 1.7 | ||||
| - 1.8 | ||||
| - 1.9 | ||||
| - tip | ||||
| 
 | ||||
| go_import_path: gopkg.in/square/go-jose.v2 | ||||
| 
 | ||||
| before_script: | ||||
| - export PATH=$HOME/.local/bin:$PATH | ||||
| 
 | ||||
| before_install: | ||||
| # Install encrypted gitcookies to get around bandwidth-limits | ||||
| # that is causing Travis-CI builds to fail. For more info, see | ||||
| # https://github.com/golang/go/issues/12933 | ||||
| - openssl aes-256-cbc -K $encrypted_1528c3c2cafd_key -iv $encrypted_1528c3c2cafd_iv -in .gitcookies.sh.enc -out .gitcookies.sh -d || true | ||||
| - bash .gitcookies.sh || true | ||||
| - go get github.com/wadey/gocovmerge | ||||
| - go get github.com/mattn/goveralls | ||||
| - go get github.com/stretchr/testify/assert | ||||
| - go get golang.org/x/tools/cmd/cover || true | ||||
| - go get code.google.com/p/go.tools/cmd/cover || true | ||||
| - pip install cram --user | ||||
| 
 | ||||
| script: | ||||
| - go test . -v -covermode=count -coverprofile=profile.cov | ||||
| - go test ./cipher -v -covermode=count -coverprofile=cipher/profile.cov | ||||
| - go test ./jwt -v -covermode=count -coverprofile=jwt/profile.cov | ||||
| - go test ./json -v # no coverage for forked encoding/json package | ||||
| - cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t | ||||
| - cd .. | ||||
| 
 | ||||
| after_success: | ||||
| - gocovmerge *.cov */*.cov > merged.coverprofile | ||||
| - $HOME/gopath/bin/goveralls -coverprofile merged.coverprofile -service=travis-ci | ||||
| 
 | ||||
|  | @ -0,0 +1,10 @@ | |||
| Serious about security | ||||
| ====================== | ||||
| 
 | ||||
| Square recognizes the important contributions the security research community | ||||
| can make. We therefore encourage reporting security issues with the code | ||||
| contained in this repository. | ||||
| 
 | ||||
| If you believe you have discovered a security vulnerability, please follow the | ||||
| guidelines at <https://hackerone.com/square-open-source>. | ||||
| 
 | ||||
|  | @ -0,0 +1,14 @@ | |||
| # Contributing | ||||
| 
 | ||||
| If you would like to contribute code to go-jose you can do so through GitHub by | ||||
| forking the repository and sending a pull request. | ||||
| 
 | ||||
| When submitting code, please make every effort to follow existing conventions | ||||
| and style in order to keep the code as readable as possible. Please also make | ||||
| sure all tests pass by running `go test`, and format your code with `go fmt`. | ||||
| We also recommend using `golint` and `errcheck`. | ||||
| 
 | ||||
| Before your code can be accepted into the project you must also sign the | ||||
| [Individual Contributor License Agreement][1]. | ||||
| 
 | ||||
|  [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 | ||||
|  | @ -0,0 +1,202 @@ | |||
| 
 | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | @ -0,0 +1,119 @@ | |||
| # Go JOSE  | ||||
| 
 | ||||
| [](https://godoc.org/gopkg.in/square/go-jose.v1) | ||||
| [](https://godoc.org/gopkg.in/square/go-jose.v2) | ||||
| [](https://raw.githubusercontent.com/square/go-jose/master/LICENSE) | ||||
| [](https://travis-ci.org/square/go-jose) | ||||
| [](https://coveralls.io/r/square/go-jose) | ||||
| 
 | ||||
| Package jose aims to provide an implementation of the Javascript Object Signing | ||||
| and Encryption set of standards. This includes support for JSON Web Encryption, | ||||
| JSON Web Signature, and JSON Web Token standards. | ||||
| 
 | ||||
| **Disclaimer**: This library contains encryption software that is subject to | ||||
| the U.S. Export Administration Regulations. You may not export, re-export, | ||||
| transfer or download this code or any part of it in violation of any United | ||||
| States law, directive or regulation. In particular this software may not be | ||||
| exported or re-exported in any form or on any media to Iran, North Sudan, | ||||
| Syria, Cuba, or North Korea, or to denied persons or entities mentioned on any | ||||
| US maintained blocked list. | ||||
| 
 | ||||
| ## Overview | ||||
| 
 | ||||
| The implementation follows the | ||||
| [JSON Web Encryption](http://dx.doi.org/10.17487/RFC7516) (RFC 7516), | ||||
| [JSON Web Signature](http://dx.doi.org/10.17487/RFC7515) (RFC 7515), and | ||||
| [JSON Web Token](http://dx.doi.org/10.17487/RFC7519) (RFC 7519). | ||||
| Tables of supported algorithms are shown below. The library supports both | ||||
| the compact and full serialization formats, and has optional support for | ||||
| multiple recipients. It also comes with a small command-line utility | ||||
| ([`jose-util`](https://github.com/square/go-jose/tree/v2/jose-util)) | ||||
| for dealing with JOSE messages in a shell. | ||||
| 
 | ||||
| **Note**: We use a forked version of the `encoding/json` package from the Go | ||||
| standard library which uses case-sensitive matching for member names (instead | ||||
| of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html)). | ||||
| This is to avoid differences in interpretation of messages between go-jose and | ||||
| libraries in other languages. | ||||
| 
 | ||||
| ### Versions | ||||
| 
 | ||||
| We use [gopkg.in](https://gopkg.in) for versioning. | ||||
| 
 | ||||
| [Version 1](https://gopkg.in/square/go-jose.v1) is the old stable version: | ||||
| 
 | ||||
|     import "gopkg.in/square/go-jose.v1" | ||||
| 
 | ||||
| [Version 2](https://gopkg.in/square/go-jose.v2) is for new development: | ||||
| 
 | ||||
|     import "gopkg.in/square/go-jose.v2" | ||||
| 
 | ||||
| The interface for [go-jose.v1](https://gopkg.in/square/go-jose.v1) will remain | ||||
| backwards compatible. No new feature development will take place on the `v1` branch, | ||||
| however bug fixes and security fixes will be backported. | ||||
| 
 | ||||
| The interface for [go-jose.v2](https://gopkg.in/square/go-jose.v2) is mostly  | ||||
| stable, but we suggest pinning to a particular revision for now as we still reserve | ||||
| the right to make changes. New feature development happens on this branch. | ||||
| 
 | ||||
| New in [go-jose.v2](https://gopkg.in/square/go-jose.v2) is a | ||||
| [jwt](https://godoc.org/gopkg.in/square/go-jose.v2/jwt) sub-package | ||||
| contributed by [@shaxbee](https://github.com/shaxbee). | ||||
| 
 | ||||
| ### Supported algorithms | ||||
| 
 | ||||
| See below for a table of supported algorithms. Algorithm identifiers match | ||||
| the names in the [JSON Web Algorithms](http://dx.doi.org/10.17487/RFC7518) | ||||
| standard where possible. The Godoc reference has a list of constants. | ||||
| 
 | ||||
|  Key encryption             | Algorithm identifier(s) | ||||
|  :------------------------- | :------------------------------ | ||||
|  RSA-PKCS#1v1.5             | RSA1_5 | ||||
|  RSA-OAEP                   | RSA-OAEP, RSA-OAEP-256 | ||||
|  AES key wrap               | A128KW, A192KW, A256KW | ||||
|  AES-GCM key wrap           | A128GCMKW, A192GCMKW, A256GCMKW | ||||
|  ECDH-ES + AES key wrap     | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | ||||
|  ECDH-ES (direct)           | ECDH-ES<sup>1</sup> | ||||
|  Direct encryption          | dir<sup>1</sup> | ||||
| 
 | ||||
| <sup>1. Not supported in multi-recipient mode</sup> | ||||
| 
 | ||||
|  Signing / MAC              | Algorithm identifier(s) | ||||
|  :------------------------- | :------------------------------ | ||||
|  RSASSA-PKCS#1v1.5          | RS256, RS384, RS512 | ||||
|  RSASSA-PSS                 | PS256, PS384, PS512 | ||||
|  HMAC                       | HS256, HS384, HS512 | ||||
|  ECDSA                      | ES256, ES384, ES512 | ||||
| 
 | ||||
|  Content encryption         | Algorithm identifier(s) | ||||
|  :------------------------- | :------------------------------ | ||||
|  AES-CBC+HMAC               | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 | ||||
|  AES-GCM                    | A128GCM, A192GCM, A256GCM  | ||||
| 
 | ||||
|  Compression                | Algorithm identifiers(s) | ||||
|  :------------------------- | ------------------------------- | ||||
|  DEFLATE (RFC 1951)         | DEF | ||||
| 
 | ||||
| ### Supported key types | ||||
| 
 | ||||
| See below for a table of supported key types. These are understood by the | ||||
| library, and can be passed to corresponding functions such as `NewEncrypter` or | ||||
| `NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which | ||||
| allows attaching a key id. | ||||
| 
 | ||||
|  Algorithm(s)               | Corresponding types | ||||
|  :------------------------- | ------------------------------- | ||||
|  RSA                        | *[rsa.PublicKey](http://golang.org/pkg/crypto/rsa/#PublicKey), *[rsa.PrivateKey](http://golang.org/pkg/crypto/rsa/#PrivateKey) | ||||
|  ECDH, ECDSA                | *[ecdsa.PublicKey](http://golang.org/pkg/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](http://golang.org/pkg/crypto/ecdsa/#PrivateKey) | ||||
|  AES, HMAC                  | []byte | ||||
| 
 | ||||
| ## Examples | ||||
| 
 | ||||
| [](https://godoc.org/gopkg.in/square/go-jose.v1) | ||||
| [](https://godoc.org/gopkg.in/square/go-jose.v2) | ||||
| 
 | ||||
| Examples can be found in the Godoc | ||||
| reference for this package. The | ||||
| [`jose-util`](https://github.com/square/go-jose/tree/v2/jose-util) | ||||
| subdirectory also contains a small command-line utility which might be useful | ||||
| as an example. | ||||
|  | @ -0,0 +1,591 @@ | |||
| /*- | ||||
|  * Copyright 2014 Square Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"crypto/aes" | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 
 | ||||
| 	"golang.org/x/crypto/ed25519" | ||||
| 	"gopkg.in/square/go-jose.v2/cipher" | ||||
| 	"gopkg.in/square/go-jose.v2/json" | ||||
| ) | ||||
| 
 | ||||
| // A generic RSA-based encrypter/verifier
 | ||||
| type rsaEncrypterVerifier struct { | ||||
| 	publicKey *rsa.PublicKey | ||||
| } | ||||
| 
 | ||||
| // A generic RSA-based decrypter/signer
 | ||||
| type rsaDecrypterSigner struct { | ||||
| 	privateKey *rsa.PrivateKey | ||||
| } | ||||
| 
 | ||||
| // A generic EC-based encrypter/verifier
 | ||||
| type ecEncrypterVerifier struct { | ||||
| 	publicKey *ecdsa.PublicKey | ||||
| } | ||||
| 
 | ||||
| type edEncrypterVerifier struct { | ||||
| 	publicKey ed25519.PublicKey | ||||
| } | ||||
| 
 | ||||
| // A key generator for ECDH-ES
 | ||||
| type ecKeyGenerator struct { | ||||
| 	size      int | ||||
| 	algID     string | ||||
| 	publicKey *ecdsa.PublicKey | ||||
| } | ||||
| 
 | ||||
| // A generic EC-based decrypter/signer
 | ||||
| type ecDecrypterSigner struct { | ||||
| 	privateKey *ecdsa.PrivateKey | ||||
| } | ||||
| 
 | ||||
| type edDecrypterSigner struct { | ||||
| 	privateKey ed25519.PrivateKey | ||||
| } | ||||
| 
 | ||||
| // newRSARecipient creates recipientKeyInfo based on the given key.
 | ||||
| func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { | ||||
| 	// Verify that key management algorithm is supported by this encrypter
 | ||||
| 	switch keyAlg { | ||||
| 	case RSA1_5, RSA_OAEP, RSA_OAEP_256: | ||||
| 	default: | ||||
| 		return recipientKeyInfo{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	if publicKey == nil { | ||||
| 		return recipientKeyInfo{}, errors.New("invalid public key") | ||||
| 	} | ||||
| 
 | ||||
| 	return recipientKeyInfo{ | ||||
| 		keyAlg: keyAlg, | ||||
| 		keyEncrypter: &rsaEncrypterVerifier{ | ||||
| 			publicKey: publicKey, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // newRSASigner creates a recipientSigInfo based on the given key.
 | ||||
| func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { | ||||
| 	// Verify that key management algorithm is supported by this encrypter
 | ||||
| 	switch sigAlg { | ||||
| 	case RS256, RS384, RS512, PS256, PS384, PS512: | ||||
| 	default: | ||||
| 		return recipientSigInfo{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	if privateKey == nil { | ||||
| 		return recipientSigInfo{}, errors.New("invalid private key") | ||||
| 	} | ||||
| 
 | ||||
| 	return recipientSigInfo{ | ||||
| 		sigAlg: sigAlg, | ||||
| 		publicKey: &JSONWebKey{ | ||||
| 			Key: &privateKey.PublicKey, | ||||
| 		}, | ||||
| 		signer: &rsaDecrypterSigner{ | ||||
| 			privateKey: privateKey, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func newEd25519Signer(sigAlg SignatureAlgorithm, privateKey ed25519.PrivateKey) (recipientSigInfo, error) { | ||||
| 	if sigAlg != EdDSA { | ||||
| 		return recipientSigInfo{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	if privateKey == nil { | ||||
| 		return recipientSigInfo{}, errors.New("invalid private key") | ||||
| 	} | ||||
| 	return recipientSigInfo{ | ||||
| 		sigAlg: sigAlg, | ||||
| 		publicKey: &JSONWebKey{ | ||||
| 			Key: privateKey.Public(), | ||||
| 		}, | ||||
| 		signer: &edDecrypterSigner{ | ||||
| 			privateKey: privateKey, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // newECDHRecipient creates recipientKeyInfo based on the given key.
 | ||||
| func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { | ||||
| 	// Verify that key management algorithm is supported by this encrypter
 | ||||
| 	switch keyAlg { | ||||
| 	case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: | ||||
| 	default: | ||||
| 		return recipientKeyInfo{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	if publicKey == nil || !publicKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { | ||||
| 		return recipientKeyInfo{}, errors.New("invalid public key") | ||||
| 	} | ||||
| 
 | ||||
| 	return recipientKeyInfo{ | ||||
| 		keyAlg: keyAlg, | ||||
| 		keyEncrypter: &ecEncrypterVerifier{ | ||||
| 			publicKey: publicKey, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // newECDSASigner creates a recipientSigInfo based on the given key.
 | ||||
| func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { | ||||
| 	// Verify that key management algorithm is supported by this encrypter
 | ||||
| 	switch sigAlg { | ||||
| 	case ES256, ES384, ES512: | ||||
| 	default: | ||||
| 		return recipientSigInfo{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	if privateKey == nil { | ||||
| 		return recipientSigInfo{}, errors.New("invalid private key") | ||||
| 	} | ||||
| 
 | ||||
| 	return recipientSigInfo{ | ||||
| 		sigAlg: sigAlg, | ||||
| 		publicKey: &JSONWebKey{ | ||||
| 			Key: &privateKey.PublicKey, | ||||
| 		}, | ||||
| 		signer: &ecDecrypterSigner{ | ||||
| 			privateKey: privateKey, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Encrypt the given payload and update the object.
 | ||||
| func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { | ||||
| 	encryptedKey, err := ctx.encrypt(cek, alg) | ||||
| 	if err != nil { | ||||
| 		return recipientInfo{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return recipientInfo{ | ||||
| 		encryptedKey: encryptedKey, | ||||
| 		header:       &rawHeader{}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Encrypt the given payload. Based on the key encryption algorithm,
 | ||||
| // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256).
 | ||||
| func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { | ||||
| 	switch alg { | ||||
| 	case RSA1_5: | ||||
| 		return rsa.EncryptPKCS1v15(randReader, ctx.publicKey, cek) | ||||
| 	case RSA_OAEP: | ||||
| 		return rsa.EncryptOAEP(sha1.New(), randReader, ctx.publicKey, cek, []byte{}) | ||||
| 	case RSA_OAEP_256: | ||||
| 		return rsa.EncryptOAEP(sha256.New(), randReader, ctx.publicKey, cek, []byte{}) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, ErrUnsupportedAlgorithm | ||||
| } | ||||
| 
 | ||||
| // Decrypt the given payload and return the content encryption key.
 | ||||
| func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { | ||||
| 	return ctx.decrypt(recipient.encryptedKey, headers.getAlgorithm(), generator) | ||||
| } | ||||
| 
 | ||||
| // Decrypt the given payload. Based on the key encryption algorithm,
 | ||||
| // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256).
 | ||||
| func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { | ||||
| 	// Note: The random reader on decrypt operations is only used for blinding,
 | ||||
| 	// so stubbing is meanlingless (hence the direct use of rand.Reader).
 | ||||
| 	switch alg { | ||||
| 	case RSA1_5: | ||||
| 		defer func() { | ||||
| 			// DecryptPKCS1v15SessionKey sometimes panics on an invalid payload
 | ||||
| 			// because of an index out of bounds error, which we want to ignore.
 | ||||
| 			// This has been fixed in Go 1.3.1 (released 2014/08/13), the recover()
 | ||||
| 			// only exists for preventing crashes with unpatched versions.
 | ||||
| 			// See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k
 | ||||
| 			// See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33
 | ||||
| 			_ = recover() | ||||
| 		}() | ||||
| 
 | ||||
| 		// Perform some input validation.
 | ||||
| 		keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 | ||||
| 		if keyBytes != len(jek) { | ||||
| 			// Input size is incorrect, the encrypted payload should always match
 | ||||
| 			// the size of the public modulus (e.g. using a 2048 bit key will
 | ||||
| 			// produce 256 bytes of output). Reject this since it's invalid input.
 | ||||
| 			return nil, ErrCryptoFailure | ||||
| 		} | ||||
| 
 | ||||
| 		cek, _, err := generator.genKey() | ||||
| 		if err != nil { | ||||
| 			return nil, ErrCryptoFailure | ||||
| 		} | ||||
| 
 | ||||
| 		// When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to
 | ||||
| 		// prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing
 | ||||
| 		// the Million Message Attack on Cryptographic Message Syntax". We are
 | ||||
| 		// therefore deliberately ignoring errors here.
 | ||||
| 		_ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) | ||||
| 
 | ||||
| 		return cek, nil | ||||
| 	case RSA_OAEP: | ||||
| 		// Use rand.Reader for RSA blinding
 | ||||
| 		return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) | ||||
| 	case RSA_OAEP_256: | ||||
| 		// Use rand.Reader for RSA blinding
 | ||||
| 		return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, ErrUnsupportedAlgorithm | ||||
| } | ||||
| 
 | ||||
| // Sign the given payload
 | ||||
| func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { | ||||
| 	var hash crypto.Hash | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case RS256, PS256: | ||||
| 		hash = crypto.SHA256 | ||||
| 	case RS384, PS384: | ||||
| 		hash = crypto.SHA384 | ||||
| 	case RS512, PS512: | ||||
| 		hash = crypto.SHA512 | ||||
| 	default: | ||||
| 		return Signature{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	hasher := hash.New() | ||||
| 
 | ||||
| 	// According to documentation, Write() on hash never fails
 | ||||
| 	_, _ = hasher.Write(payload) | ||||
| 	hashed := hasher.Sum(nil) | ||||
| 
 | ||||
| 	var out []byte | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case RS256, RS384, RS512: | ||||
| 		out, err = rsa.SignPKCS1v15(randReader, ctx.privateKey, hash, hashed) | ||||
| 	case PS256, PS384, PS512: | ||||
| 		out, err = rsa.SignPSS(randReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ | ||||
| 			SaltLength: rsa.PSSSaltLengthAuto, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return Signature{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return Signature{ | ||||
| 		Signature: out, | ||||
| 		protected: &rawHeader{}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Verify the given payload
 | ||||
| func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { | ||||
| 	var hash crypto.Hash | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case RS256, PS256: | ||||
| 		hash = crypto.SHA256 | ||||
| 	case RS384, PS384: | ||||
| 		hash = crypto.SHA384 | ||||
| 	case RS512, PS512: | ||||
| 		hash = crypto.SHA512 | ||||
| 	default: | ||||
| 		return ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	hasher := hash.New() | ||||
| 
 | ||||
| 	// According to documentation, Write() on hash never fails
 | ||||
| 	_, _ = hasher.Write(payload) | ||||
| 	hashed := hasher.Sum(nil) | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case RS256, RS384, RS512: | ||||
| 		return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) | ||||
| 	case PS256, PS384, PS512: | ||||
| 		return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) | ||||
| 	} | ||||
| 
 | ||||
| 	return ErrUnsupportedAlgorithm | ||||
| } | ||||
| 
 | ||||
| // Encrypt the given payload and update the object.
 | ||||
| func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { | ||||
| 	switch alg { | ||||
| 	case ECDH_ES: | ||||
| 		// ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key.
 | ||||
| 		return recipientInfo{ | ||||
| 			header: &rawHeader{}, | ||||
| 		}, nil | ||||
| 	case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: | ||||
| 	default: | ||||
| 		return recipientInfo{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	generator := ecKeyGenerator{ | ||||
| 		algID:     string(alg), | ||||
| 		publicKey: ctx.publicKey, | ||||
| 	} | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case ECDH_ES_A128KW: | ||||
| 		generator.size = 16 | ||||
| 	case ECDH_ES_A192KW: | ||||
| 		generator.size = 24 | ||||
| 	case ECDH_ES_A256KW: | ||||
| 		generator.size = 32 | ||||
| 	} | ||||
| 
 | ||||
| 	kek, header, err := generator.genKey() | ||||
| 	if err != nil { | ||||
| 		return recipientInfo{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	block, err := aes.NewCipher(kek) | ||||
| 	if err != nil { | ||||
| 		return recipientInfo{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	jek, err := josecipher.KeyWrap(block, cek) | ||||
| 	if err != nil { | ||||
| 		return recipientInfo{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return recipientInfo{ | ||||
| 		encryptedKey: jek, | ||||
| 		header:       &header, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Get key size for EC key generator
 | ||||
| func (ctx ecKeyGenerator) keySize() int { | ||||
| 	return ctx.size | ||||
| } | ||||
| 
 | ||||
| // Get a content encryption key for ECDH-ES
 | ||||
| func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { | ||||
| 	priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, randReader) | ||||
| 	if err != nil { | ||||
| 		return nil, rawHeader{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) | ||||
| 
 | ||||
| 	b, err := json.Marshal(&JSONWebKey{ | ||||
| 		Key: &priv.PublicKey, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	headers := rawHeader{ | ||||
| 		headerEPK: makeRawMessage(b), | ||||
| 	} | ||||
| 
 | ||||
| 	return out, headers, nil | ||||
| } | ||||
| 
 | ||||
| // Decrypt the given payload and return the content encryption key.
 | ||||
| func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { | ||||
| 	epk, err := headers.getEPK() | ||||
| 	if err != nil { | ||||
| 		return nil, errors.New("square/go-jose: invalid epk header") | ||||
| 	} | ||||
| 	if epk == nil { | ||||
| 		return nil, errors.New("square/go-jose: missing epk header") | ||||
| 	} | ||||
| 
 | ||||
| 	publicKey, ok := epk.Key.(*ecdsa.PublicKey) | ||||
| 	if publicKey == nil || !ok { | ||||
| 		return nil, errors.New("square/go-jose: invalid epk header") | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { | ||||
| 		return nil, errors.New("square/go-jose: invalid public key in epk header") | ||||
| 	} | ||||
| 
 | ||||
| 	apuData, err := headers.getAPU() | ||||
| 	if err != nil { | ||||
| 		return nil, errors.New("square/go-jose: invalid apu header") | ||||
| 	} | ||||
| 	apvData, err := headers.getAPV() | ||||
| 	if err != nil { | ||||
| 		return nil, errors.New("square/go-jose: invalid apv header") | ||||
| 	} | ||||
| 
 | ||||
| 	deriveKey := func(algID string, size int) []byte { | ||||
| 		return josecipher.DeriveECDHES(algID, apuData.bytes(), apvData.bytes(), ctx.privateKey, publicKey, size) | ||||
| 	} | ||||
| 
 | ||||
| 	var keySize int | ||||
| 
 | ||||
| 	algorithm := headers.getAlgorithm() | ||||
| 	switch algorithm { | ||||
| 	case ECDH_ES: | ||||
| 		// ECDH-ES uses direct key agreement, no key unwrapping necessary.
 | ||||
| 		return deriveKey(string(headers.getEncryption()), generator.keySize()), nil | ||||
| 	case ECDH_ES_A128KW: | ||||
| 		keySize = 16 | ||||
| 	case ECDH_ES_A192KW: | ||||
| 		keySize = 24 | ||||
| 	case ECDH_ES_A256KW: | ||||
| 		keySize = 32 | ||||
| 	default: | ||||
| 		return nil, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	key := deriveKey(string(algorithm), keySize) | ||||
| 	block, err := aes.NewCipher(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return josecipher.KeyUnwrap(block, recipient.encryptedKey) | ||||
| } | ||||
| func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { | ||||
| 	if alg != EdDSA { | ||||
| 		return Signature{}, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	sig, err := ctx.privateKey.Sign(randReader, payload, crypto.Hash(0)) | ||||
| 	if err != nil { | ||||
| 		return Signature{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return Signature{ | ||||
| 		Signature: sig, | ||||
| 		protected: &rawHeader{}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (ctx edEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { | ||||
| 	if alg != EdDSA { | ||||
| 		return ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 	ok := ed25519.Verify(ctx.publicKey, payload, signature) | ||||
| 	if !ok { | ||||
| 		return errors.New("square/go-jose: ed25519 signature failed to verify") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Sign the given payload
 | ||||
| func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { | ||||
| 	var expectedBitSize int | ||||
| 	var hash crypto.Hash | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case ES256: | ||||
| 		expectedBitSize = 256 | ||||
| 		hash = crypto.SHA256 | ||||
| 	case ES384: | ||||
| 		expectedBitSize = 384 | ||||
| 		hash = crypto.SHA384 | ||||
| 	case ES512: | ||||
| 		expectedBitSize = 521 | ||||
| 		hash = crypto.SHA512 | ||||
| 	} | ||||
| 
 | ||||
| 	curveBits := ctx.privateKey.Curve.Params().BitSize | ||||
| 	if expectedBitSize != curveBits { | ||||
| 		return Signature{}, fmt.Errorf("square/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) | ||||
| 	} | ||||
| 
 | ||||
| 	hasher := hash.New() | ||||
| 
 | ||||
| 	// According to documentation, Write() on hash never fails
 | ||||
| 	_, _ = hasher.Write(payload) | ||||
| 	hashed := hasher.Sum(nil) | ||||
| 
 | ||||
| 	r, s, err := ecdsa.Sign(randReader, ctx.privateKey, hashed) | ||||
| 	if err != nil { | ||||
| 		return Signature{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	keyBytes := curveBits / 8 | ||||
| 	if curveBits%8 > 0 { | ||||
| 		keyBytes++ | ||||
| 	} | ||||
| 
 | ||||
| 	// We serialize the outpus (r and s) into big-endian byte arrays and pad
 | ||||
| 	// them with zeros on the left to make sure the sizes work out. Both arrays
 | ||||
| 	// must be keyBytes long, and the output must be 2*keyBytes long.
 | ||||
| 	rBytes := r.Bytes() | ||||
| 	rBytesPadded := make([]byte, keyBytes) | ||||
| 	copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) | ||||
| 
 | ||||
| 	sBytes := s.Bytes() | ||||
| 	sBytesPadded := make([]byte, keyBytes) | ||||
| 	copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) | ||||
| 
 | ||||
| 	out := append(rBytesPadded, sBytesPadded...) | ||||
| 
 | ||||
| 	return Signature{ | ||||
| 		Signature: out, | ||||
| 		protected: &rawHeader{}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Verify the given payload
 | ||||
| func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { | ||||
| 	var keySize int | ||||
| 	var hash crypto.Hash | ||||
| 
 | ||||
| 	switch alg { | ||||
| 	case ES256: | ||||
| 		keySize = 32 | ||||
| 		hash = crypto.SHA256 | ||||
| 	case ES384: | ||||
| 		keySize = 48 | ||||
| 		hash = crypto.SHA384 | ||||
| 	case ES512: | ||||
| 		keySize = 66 | ||||
| 		hash = crypto.SHA512 | ||||
| 	default: | ||||
| 		return ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	if len(signature) != 2*keySize { | ||||
| 		return fmt.Errorf("square/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) | ||||
| 	} | ||||
| 
 | ||||
| 	hasher := hash.New() | ||||
| 
 | ||||
| 	// According to documentation, Write() on hash never fails
 | ||||
| 	_, _ = hasher.Write(payload) | ||||
| 	hashed := hasher.Sum(nil) | ||||
| 
 | ||||
| 	r := big.NewInt(0).SetBytes(signature[:keySize]) | ||||
| 	s := big.NewInt(0).SetBytes(signature[keySize:]) | ||||
| 
 | ||||
| 	match := ecdsa.Verify(ctx.publicKey, hashed, r, s) | ||||
| 	if !match { | ||||
| 		return errors.New("square/go-jose: ecdsa signature failed to verify") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -0,0 +1,196 @@ | |||
| /*- | ||||
|  * Copyright 2014 Square Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package josecipher | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/cipher" | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/sha256" | ||||
| 	"crypto/sha512" | ||||
| 	"crypto/subtle" | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| 	"hash" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	nonceBytes = 16 | ||||
| ) | ||||
| 
 | ||||
| // NewCBCHMAC instantiates a new AEAD based on CBC+HMAC.
 | ||||
| func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { | ||||
| 	keySize := len(key) / 2 | ||||
| 	integrityKey := key[:keySize] | ||||
| 	encryptionKey := key[keySize:] | ||||
| 
 | ||||
| 	blockCipher, err := newBlockCipher(encryptionKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var hash func() hash.Hash | ||||
| 	switch keySize { | ||||
| 	case 16: | ||||
| 		hash = sha256.New | ||||
| 	case 24: | ||||
| 		hash = sha512.New384 | ||||
| 	case 32: | ||||
| 		hash = sha512.New | ||||
| 	} | ||||
| 
 | ||||
| 	return &cbcAEAD{ | ||||
| 		hash:         hash, | ||||
| 		blockCipher:  blockCipher, | ||||
| 		authtagBytes: keySize, | ||||
| 		integrityKey: integrityKey, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // An AEAD based on CBC+HMAC
 | ||||
| type cbcAEAD struct { | ||||
| 	hash         func() hash.Hash | ||||
| 	authtagBytes int | ||||
| 	integrityKey []byte | ||||
| 	blockCipher  cipher.Block | ||||
| } | ||||
| 
 | ||||
| func (ctx *cbcAEAD) NonceSize() int { | ||||
| 	return nonceBytes | ||||
| } | ||||
| 
 | ||||
| func (ctx *cbcAEAD) Overhead() int { | ||||
| 	// Maximum overhead is block size (for padding) plus auth tag length, where
 | ||||
| 	// the length of the auth tag is equivalent to the key size.
 | ||||
| 	return ctx.blockCipher.BlockSize() + ctx.authtagBytes | ||||
| } | ||||
| 
 | ||||
| // Seal encrypts and authenticates the plaintext.
 | ||||
| func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { | ||||
| 	// Output buffer -- must take care not to mangle plaintext input.
 | ||||
| 	ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] | ||||
| 	copy(ciphertext, plaintext) | ||||
| 	ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) | ||||
| 
 | ||||
| 	cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) | ||||
| 
 | ||||
| 	cbc.CryptBlocks(ciphertext, ciphertext) | ||||
| 	authtag := ctx.computeAuthTag(data, nonce, ciphertext) | ||||
| 
 | ||||
| 	ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) | ||||
| 	copy(out, ciphertext) | ||||
| 	copy(out[len(ciphertext):], authtag) | ||||
| 
 | ||||
| 	return ret | ||||
| } | ||||
| 
 | ||||
| // Open decrypts and authenticates the ciphertext.
 | ||||
| func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { | ||||
| 	if len(ciphertext) < ctx.authtagBytes { | ||||
| 		return nil, errors.New("square/go-jose: invalid ciphertext (too short)") | ||||
| 	} | ||||
| 
 | ||||
| 	offset := len(ciphertext) - ctx.authtagBytes | ||||
| 	expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) | ||||
| 	match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) | ||||
| 	if match != 1 { | ||||
| 		return nil, errors.New("square/go-jose: invalid ciphertext (auth tag mismatch)") | ||||
| 	} | ||||
| 
 | ||||
| 	cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) | ||||
| 
 | ||||
| 	// Make copy of ciphertext buffer, don't want to modify in place
 | ||||
| 	buffer := append([]byte{}, []byte(ciphertext[:offset])...) | ||||
| 
 | ||||
| 	if len(buffer)%ctx.blockCipher.BlockSize() > 0 { | ||||
| 		return nil, errors.New("square/go-jose: invalid ciphertext (invalid length)") | ||||
| 	} | ||||
| 
 | ||||
| 	cbc.CryptBlocks(buffer, buffer) | ||||
| 
 | ||||
| 	// Remove padding
 | ||||
| 	plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) | ||||
| 	copy(out, plaintext) | ||||
| 
 | ||||
| 	return ret, nil | ||||
| } | ||||
| 
 | ||||
| // Compute an authentication tag
 | ||||
| func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { | ||||
| 	buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) | ||||
| 	n := 0 | ||||
| 	n += copy(buffer, aad) | ||||
| 	n += copy(buffer[n:], nonce) | ||||
| 	n += copy(buffer[n:], ciphertext) | ||||
| 	binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) | ||||
| 
 | ||||
| 	// According to documentation, Write() on hash.Hash never fails.
 | ||||
| 	hmac := hmac.New(ctx.hash, ctx.integrityKey) | ||||
| 	_, _ = hmac.Write(buffer) | ||||
| 
 | ||||
| 	return hmac.Sum(nil)[:ctx.authtagBytes] | ||||
| } | ||||
| 
 | ||||
| // resize ensures the the given slice has a capacity of at least n bytes.
 | ||||
| // If the capacity of the slice is less than n, a new slice is allocated
 | ||||
| // and the existing data will be copied.
 | ||||
| func resize(in []byte, n uint64) (head, tail []byte) { | ||||
| 	if uint64(cap(in)) >= n { | ||||
| 		head = in[:n] | ||||
| 	} else { | ||||
| 		head = make([]byte, n) | ||||
| 		copy(head, in) | ||||
| 	} | ||||
| 
 | ||||
| 	tail = head[len(in):] | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // Apply padding
 | ||||
| func padBuffer(buffer []byte, blockSize int) []byte { | ||||
| 	missing := blockSize - (len(buffer) % blockSize) | ||||
| 	ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) | ||||
| 	padding := bytes.Repeat([]byte{byte(missing)}, missing) | ||||
| 	copy(out, padding) | ||||
| 	return ret | ||||
| } | ||||
| 
 | ||||
| // Remove padding
 | ||||
| func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { | ||||
| 	if len(buffer)%blockSize != 0 { | ||||
| 		return nil, errors.New("square/go-jose: invalid padding") | ||||
| 	} | ||||
| 
 | ||||
| 	last := buffer[len(buffer)-1] | ||||
| 	count := int(last) | ||||
| 
 | ||||
| 	if count == 0 || count > blockSize || count > len(buffer) { | ||||
| 		return nil, errors.New("square/go-jose: invalid padding") | ||||
| 	} | ||||
| 
 | ||||
| 	padding := bytes.Repeat([]byte{last}, count) | ||||
| 	if !bytes.HasSuffix(buffer, padding) { | ||||
| 		return nil, errors.New("square/go-jose: invalid padding") | ||||
| 	} | ||||
| 
 | ||||
| 	return buffer[:len(buffer)-count], nil | ||||
| } | ||||
|  | @ -0,0 +1,75 @@ | |||
| /*- | ||||
|  * Copyright 2014 Square Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package josecipher | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"encoding/binary" | ||||
| 	"hash" | ||||
| 	"io" | ||||
| ) | ||||
| 
 | ||||
| type concatKDF struct { | ||||
| 	z, info []byte | ||||
| 	i       uint32 | ||||
| 	cache   []byte | ||||
| 	hasher  hash.Hash | ||||
| } | ||||
| 
 | ||||
| // NewConcatKDF builds a KDF reader based on the given inputs.
 | ||||
| func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { | ||||
| 	buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) | ||||
| 	n := 0 | ||||
| 	n += copy(buffer, algID) | ||||
| 	n += copy(buffer[n:], ptyUInfo) | ||||
| 	n += copy(buffer[n:], ptyVInfo) | ||||
| 	n += copy(buffer[n:], supPubInfo) | ||||
| 	copy(buffer[n:], supPrivInfo) | ||||
| 
 | ||||
| 	hasher := hash.New() | ||||
| 
 | ||||
| 	return &concatKDF{ | ||||
| 		z:      z, | ||||
| 		info:   buffer, | ||||
| 		hasher: hasher, | ||||
| 		cache:  []byte{}, | ||||
| 		i:      1, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (ctx *concatKDF) Read(out []byte) (int, error) { | ||||
| 	copied := copy(out, ctx.cache) | ||||
| 	ctx.cache = ctx.cache[copied:] | ||||
| 
 | ||||
| 	for copied < len(out) { | ||||
| 		ctx.hasher.Reset() | ||||
| 
 | ||||
| 		// Write on a hash.Hash never fails
 | ||||
| 		_ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) | ||||
| 		_, _ = ctx.hasher.Write(ctx.z) | ||||
| 		_, _ = ctx.hasher.Write(ctx.info) | ||||
| 
 | ||||
| 		hash := ctx.hasher.Sum(nil) | ||||
| 		chunkCopied := copy(out[copied:], hash) | ||||
| 		copied += chunkCopied | ||||
| 		ctx.cache = hash[chunkCopied:] | ||||
| 
 | ||||
| 		ctx.i++ | ||||
| 	} | ||||
| 
 | ||||
| 	return copied, nil | ||||
| } | ||||
|  | @ -0,0 +1,62 @@ | |||
| /*- | ||||
|  * Copyright 2014 Square Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package josecipher | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"crypto/ecdsa" | ||||
| 	"encoding/binary" | ||||
| ) | ||||
| 
 | ||||
| // DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA.
 | ||||
| // It is an error to call this function with a private/public key that are not on the same
 | ||||
| // curve. Callers must ensure that the keys are valid before calling this function. Output
 | ||||
| // size may be at most 1<<16 bytes (64 KiB).
 | ||||
| func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { | ||||
| 	if size > 1<<16 { | ||||
| 		panic("ECDH-ES output size too large, must be less than or equal to 1<<16") | ||||
| 	} | ||||
| 
 | ||||
| 	// algId, partyUInfo, partyVInfo inputs must be prefixed with the length
 | ||||
| 	algID := lengthPrefixed([]byte(alg)) | ||||
| 	ptyUInfo := lengthPrefixed(apuData) | ||||
| 	ptyVInfo := lengthPrefixed(apvData) | ||||
| 
 | ||||
| 	// suppPubInfo is the encoded length of the output size in bits
 | ||||
| 	supPubInfo := make([]byte, 4) | ||||
| 	binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) | ||||
| 
 | ||||
| 	if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { | ||||
| 		panic("public key not on same curve as private key") | ||||
| 	} | ||||
| 
 | ||||
| 	z, _ := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) | ||||
| 	reader := NewConcatKDF(crypto.SHA256, z.Bytes(), algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) | ||||
| 
 | ||||
| 	key := make([]byte, size) | ||||
| 
 | ||||
| 	// Read on the KDF will never fail
 | ||||
| 	_, _ = reader.Read(key) | ||||
| 	return key | ||||
| } | ||||
| 
 | ||||
| func lengthPrefixed(data []byte) []byte { | ||||
| 	out := make([]byte, len(data)+4) | ||||
| 	binary.BigEndian.PutUint32(out, uint32(len(data))) | ||||
| 	copy(out[4:], data) | ||||
| 	return out | ||||
| } | ||||
|  | @ -0,0 +1,109 @@ | |||
| /*- | ||||
|  * Copyright 2014 Square Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package josecipher | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/cipher" | ||||
| 	"crypto/subtle" | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| ) | ||||
| 
 | ||||
| var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} | ||||
| 
 | ||||
| // KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher.
 | ||||
| func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { | ||||
| 	if len(cek)%8 != 0 { | ||||
| 		return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") | ||||
| 	} | ||||
| 
 | ||||
| 	n := len(cek) / 8 | ||||
| 	r := make([][]byte, n) | ||||
| 
 | ||||
| 	for i := range r { | ||||
| 		r[i] = make([]byte, 8) | ||||
| 		copy(r[i], cek[i*8:]) | ||||
| 	} | ||||
| 
 | ||||
| 	buffer := make([]byte, 16) | ||||
| 	tBytes := make([]byte, 8) | ||||
| 	copy(buffer, defaultIV) | ||||
| 
 | ||||
| 	for t := 0; t < 6*n; t++ { | ||||
| 		copy(buffer[8:], r[t%n]) | ||||
| 
 | ||||
| 		block.Encrypt(buffer, buffer) | ||||
| 
 | ||||
| 		binary.BigEndian.PutUint64(tBytes, uint64(t+1)) | ||||
| 
 | ||||
| 		for i := 0; i < 8; i++ { | ||||
| 			buffer[i] = buffer[i] ^ tBytes[i] | ||||
| 		} | ||||
| 		copy(r[t%n], buffer[8:]) | ||||
| 	} | ||||
| 
 | ||||
| 	out := make([]byte, (n+1)*8) | ||||
| 	copy(out, buffer[:8]) | ||||
| 	for i := range r { | ||||
| 		copy(out[(i+1)*8:], r[i]) | ||||
| 	} | ||||
| 
 | ||||
| 	return out, nil | ||||
| } | ||||
| 
 | ||||
| // KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher.
 | ||||
| func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { | ||||
| 	if len(ciphertext)%8 != 0 { | ||||
| 		return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") | ||||
| 	} | ||||
| 
 | ||||
| 	n := (len(ciphertext) / 8) - 1 | ||||
| 	r := make([][]byte, n) | ||||
| 
 | ||||
| 	for i := range r { | ||||
| 		r[i] = make([]byte, 8) | ||||
| 		copy(r[i], ciphertext[(i+1)*8:]) | ||||
| 	} | ||||
| 
 | ||||
| 	buffer := make([]byte, 16) | ||||
| 	tBytes := make([]byte, 8) | ||||
| 	copy(buffer[:8], ciphertext[:8]) | ||||
| 
 | ||||
| 	for t := 6*n - 1; t >= 0; t-- { | ||||
| 		binary.BigEndian.PutUint64(tBytes, uint64(t+1)) | ||||
| 
 | ||||
| 		for i := 0; i < 8; i++ { | ||||
| 			buffer[i] = buffer[i] ^ tBytes[i] | ||||
| 		} | ||||
| 		copy(buffer[8:], r[t%n]) | ||||
| 
 | ||||
| 		block.Decrypt(buffer, buffer) | ||||
| 
 | ||||
| 		copy(r[t%n], buffer[8:]) | ||||
| 	} | ||||
| 
 | ||||
| 	if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { | ||||
| 		return nil, errors.New("square/go-jose: failed to unwrap key") | ||||
| 	} | ||||
| 
 | ||||
| 	out := make([]byte, n*8) | ||||
| 	for i := range r { | ||||
| 		copy(out[i*8:], r[i]) | ||||
| 	} | ||||
| 
 | ||||
| 	return out, nil | ||||
| } | ||||
|  | @ -0,0 +1,510 @@ | |||
| /*- | ||||
|  * Copyright 2014 Square Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package jose | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/rsa" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 
 | ||||
| 	"gopkg.in/square/go-jose.v2/json" | ||||
| ) | ||||
| 
 | ||||
| // Encrypter represents an encrypter which produces an encrypted JWE object.
 | ||||
| type Encrypter interface { | ||||
| 	Encrypt(plaintext []byte) (*JSONWebEncryption, error) | ||||
| 	EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) | ||||
| 	Options() EncrypterOptions | ||||
| } | ||||
| 
 | ||||
| // A generic content cipher
 | ||||
| type contentCipher interface { | ||||
| 	keySize() int | ||||
| 	encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) | ||||
| 	decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) | ||||
| } | ||||
| 
 | ||||
| // A key generator (for generating/getting a CEK)
 | ||||
| type keyGenerator interface { | ||||
| 	keySize() int | ||||
| 	genKey() ([]byte, rawHeader, error) | ||||
| } | ||||
| 
 | ||||
| // A generic key encrypter
 | ||||
| type keyEncrypter interface { | ||||
| 	encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key
 | ||||
| } | ||||
| 
 | ||||
| // A generic key decrypter
 | ||||
| type keyDecrypter interface { | ||||
| 	decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key
 | ||||
| } | ||||
| 
 | ||||
| // A generic encrypter based on the given key encrypter and content cipher.
 | ||||
| type genericEncrypter struct { | ||||
| 	contentAlg     ContentEncryption | ||||
| 	compressionAlg CompressionAlgorithm | ||||
| 	cipher         contentCipher | ||||
| 	recipients     []recipientKeyInfo | ||||
| 	keyGenerator   keyGenerator | ||||
| 	extraHeaders   map[HeaderKey]interface{} | ||||
| } | ||||
| 
 | ||||
| type recipientKeyInfo struct { | ||||
| 	keyID        string | ||||
| 	keyAlg       KeyAlgorithm | ||||
| 	keyEncrypter keyEncrypter | ||||
| } | ||||
| 
 | ||||
| // EncrypterOptions represents options that can be set on new encrypters.
 | ||||
| type EncrypterOptions struct { | ||||
| 	Compression CompressionAlgorithm | ||||
| 
 | ||||
| 	// Optional map of additional keys to be inserted into the protected header
 | ||||
| 	// of a JWS object. Some specifications which make use of JWS like to insert
 | ||||
| 	// additional values here. All values must be JSON-serializable.
 | ||||
| 	ExtraHeaders map[HeaderKey]interface{} | ||||
| } | ||||
| 
 | ||||
| // WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it
 | ||||
| // if necessary. It returns itself and so can be used in a fluent style.
 | ||||
| func (eo *EncrypterOptions) WithHeader(k HeaderKey, v interface{}) *EncrypterOptions { | ||||
| 	if eo.ExtraHeaders == nil { | ||||
| 		eo.ExtraHeaders = map[HeaderKey]interface{}{} | ||||
| 	} | ||||
| 	eo.ExtraHeaders[k] = v | ||||
| 	return eo | ||||
| } | ||||
| 
 | ||||
| // WithContentType adds a content type ("cty") header and returns the updated
 | ||||
| // EncrypterOptions.
 | ||||
| func (eo *EncrypterOptions) WithContentType(contentType ContentType) *EncrypterOptions { | ||||
| 	return eo.WithHeader(HeaderContentType, contentType) | ||||
| } | ||||
| 
 | ||||
| // WithType adds a type ("typ") header and returns the updated EncrypterOptions.
 | ||||
| func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions { | ||||
| 	return eo.WithHeader(HeaderType, typ) | ||||
| } | ||||
| 
 | ||||
| // Recipient represents an algorithm/key to encrypt messages to.
 | ||||
| type Recipient struct { | ||||
| 	Algorithm KeyAlgorithm | ||||
| 	Key       interface{} | ||||
| 	KeyID     string | ||||
| } | ||||
| 
 | ||||
| // NewEncrypter creates an appropriate encrypter based on the key type
 | ||||
| func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { | ||||
| 	encrypter := &genericEncrypter{ | ||||
| 		contentAlg: enc, | ||||
| 		recipients: []recipientKeyInfo{}, | ||||
| 		cipher:     getContentCipher(enc), | ||||
| 	} | ||||
| 	if opts != nil { | ||||
| 		encrypter.compressionAlg = opts.Compression | ||||
| 		encrypter.extraHeaders = opts.ExtraHeaders | ||||
| 	} | ||||
| 
 | ||||
| 	if encrypter.cipher == nil { | ||||
| 		return nil, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 
 | ||||
| 	var keyID string | ||||
| 	var rawKey interface{} | ||||
| 	switch encryptionKey := rcpt.Key.(type) { | ||||
| 	case JSONWebKey: | ||||
| 		keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key | ||||
| 	case *JSONWebKey: | ||||
| 		keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key | ||||
| 	default: | ||||
| 		rawKey = encryptionKey | ||||
| 	} | ||||
| 
 | ||||
| 	switch rcpt.Algorithm { | ||||
| 	case DIRECT: | ||||
| 		// Direct encryption mode must be treated differently
 | ||||
| 		if reflect.TypeOf(rawKey) != reflect.TypeOf([]byte{}) { | ||||
| 			return nil, ErrUnsupportedKeyType | ||||
| 		} | ||||
| 		encrypter.keyGenerator = staticKeyGenerator{ | ||||
| 			key: rawKey.([]byte), | ||||
| 		} | ||||
| 		recipientInfo, _ := newSymmetricRecipient(rcpt.Algorithm, rawKey.([]byte)) | ||||
| 		recipientInfo.keyID = keyID | ||||
| 		if rcpt.KeyID != "" { | ||||
| 			recipientInfo.keyID = rcpt.KeyID | ||||
| 		} | ||||
| 		encrypter.recipients = []recipientKeyInfo{recipientInfo} | ||||
| 		return encrypter, nil | ||||
| 	case ECDH_ES: | ||||
| 		// ECDH-ES (w/o key wrapping) is similar to DIRECT mode
 | ||||
| 		typeOf := reflect.TypeOf(rawKey) | ||||
| 		if typeOf != reflect.TypeOf(&ecdsa.PublicKey{}) { | ||||
| 			return nil, ErrUnsupportedKeyType | ||||
| 		} | ||||
| 		encrypter.keyGenerator = ecKeyGenerator{ | ||||
| 			size:      encrypter.cipher.keySize(), | ||||
| 			algID:     string(enc), | ||||
| 			publicKey: rawKey.(*ecdsa.PublicKey), | ||||
| 		} | ||||
| 		recipientInfo, _ := newECDHRecipient(rcpt.Algorithm, rawKey.(*ecdsa.PublicKey)) | ||||
| 		recipientInfo.keyID = keyID | ||||
| 		if rcpt.KeyID != "" { | ||||
| 			recipientInfo.keyID = rcpt.KeyID | ||||
| 		} | ||||
| 		encrypter.recipients = []recipientKeyInfo{recipientInfo} | ||||
| 		return encrypter, nil | ||||
| 	default: | ||||
| 		// Can just add a standard recipient
 | ||||
| 		encrypter.keyGenerator = randomKeyGenerator{ | ||||
| 			size: encrypter.cipher.keySize(), | ||||
| 		} | ||||
| 		err := encrypter.addRecipient(rcpt) | ||||
| 		return encrypter, err | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewMultiEncrypter creates a multi-encrypter based on the given parameters
 | ||||
| func NewMultiEncrypter(enc ContentEncryption, rcpts []Recipient, opts *EncrypterOptions) (Encrypter, error) { | ||||
| 	cipher := getContentCipher(enc) | ||||
| 
 | ||||
| 	if cipher == nil { | ||||
| 		return nil, ErrUnsupportedAlgorithm | ||||
| 	} | ||||
| 	if rcpts == nil || len(rcpts) == 0 { | ||||
| 		return nil, fmt.Errorf("square/go-jose: recipients is nil or empty") | ||||
| 	} | ||||
| 
 | ||||
| 	encrypter := &genericEncrypter{ | ||||
| 		contentAlg: enc, | ||||
| 		recipients: []recipientKeyInfo{}, | ||||
| 		cipher:     cipher, | ||||
| 		keyGenerator: randomKeyGenerator{ | ||||
| 			size: cipher.keySize(), | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	if opts != nil { | ||||
| 		encrypter.compressionAlg = opts.Compression | ||||
| 	} | ||||
| 
 | ||||
| 	for _, recipient := range rcpts { | ||||
| 		err := encrypter.addRecipient(recipient) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return encrypter, nil | ||||
| } | ||||
| 
 | ||||
| func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) { | ||||
| 	var recipientInfo recipientKeyInfo | ||||
| 
 | ||||
| 	switch recipient.Algorithm { | ||||
| 	case DIRECT, ECDH_ES: | ||||
| 		return fmt.Errorf("square/go-jose: key algorithm '%s' not supported in multi-recipient mode", recipient.Algorithm) | ||||
| 	} | ||||
| 
 | ||||
| 	recipientInfo, err = makeJWERecipient(recipient.Algorithm, recipient.Key) | ||||
| 	if recipient.KeyID != "" { | ||||
| 		recipientInfo.keyID = recipient.KeyID | ||||
| 	} | ||||
| 
 | ||||
| 	if err == nil { | ||||
| 		ctx.recipients = append(ctx.recipients, recipientInfo) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { | ||||
| 	switch encryptionKey := encryptionKey.(type) { | ||||
| 	case *rsa.PublicKey: | ||||
| 		return newRSARecipient(alg, encryptionKey) | ||||
| 	case *ecdsa.PublicKey: | ||||
| 		return newECDHRecipient(alg, encryptionKey) | ||||
| 	case []byte: | ||||
| 		return newSymmetricRecipient(alg, encryptionKey) | ||||
| 	case *JSONWebKey: | ||||
| 		recipient, err := makeJWERecipient(alg, encryptionKey.Key) | ||||
| 		recipient.keyID = encryptionKey.KeyID | ||||
| 		return recipient, err | ||||
| 	default: | ||||
| 		return recipientKeyInfo{}, ErrUnsupportedKeyType | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // newDecrypter creates an appropriate decrypter based on the key type
 | ||||
| func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { | ||||
| 	switch decryptionKey := decryptionKey.(type) { | ||||
| 	case *rsa.PrivateKey: | ||||
| 		return &rsaDecrypterSigner{ | ||||
| 			privateKey: decryptionKey, | ||||
| 		}, nil | ||||
| 	case *ecdsa.PrivateKey: | ||||
| 		return &ecDecrypterSigner{ | ||||
| 			privateKey: decryptionKey, | ||||
| 		}, nil | ||||
| 	case []byte: | ||||
| 		return &symmetricKeyCipher{ | ||||
| 			key: decryptionKey, | ||||
| 		}, nil | ||||
| 	case JSONWebKey: | ||||
| 		return newDecrypter(decryptionKey.Key) | ||||
| 	case *JSONWebKey: | ||||
| 		return newDecrypter(decryptionKey.Key) | ||||
| 	default: | ||||
| 		return nil, ErrUnsupportedKeyType | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Implementation of encrypt method producing a JWE object.
 | ||||
| func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JSONWebEncryption, error) { | ||||
| 	return ctx.EncryptWithAuthData(plaintext, nil) | ||||
| } | ||||
| 
 | ||||
| // Implementation of encrypt method producing a JWE object.
 | ||||
| func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryption, error) { | ||||
| 	obj := &JSONWebEncryption{} | ||||
| 	obj.aad = aad | ||||
| 
 | ||||
| 	obj.protected = &rawHeader{} | ||||
| 	err := obj.protected.set(headerEncryption, ctx.contentAlg) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	obj.recipients = make([]recipientInfo, len(ctx.recipients)) | ||||
| 
 | ||||
| 	if len(ctx.recipients) == 0 { | ||||
| 		return nil, fmt.Errorf("square/go-jose: no recipients to encrypt to") | ||||
| 	} | ||||
| 
 | ||||
| 	cek, headers, err := ctx.keyGenerator.genKey() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	obj.protected.merge(&headers) | ||||
| 
 | ||||
| 	for i, info := range ctx.recipients { | ||||
| 		recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		err = recipient.header.set(headerAlgorithm, info.keyAlg) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		if info.keyID != "" { | ||||
| 			err = recipient.header.set(headerKeyID, info.keyID) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 		obj.recipients[i] = recipient | ||||
| 	} | ||||
| 
 | ||||
| 	if len(ctx.recipients) == 1 { | ||||
| 		// Move per-recipient headers into main protected header if there's
 | ||||
| 		// only a single recipient.
 | ||||
| 		obj.protected.merge(obj.recipients[0].header) | ||||
| 		obj.recipients[0].header = nil | ||||
| 	} | ||||
| 
 | ||||
| 	if ctx.compressionAlg != NONE { | ||||
| 		plaintext, err = compress(ctx.compressionAlg, plaintext) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		err = obj.protected.set(headerCompression, ctx.compressionAlg) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for k, v := range ctx.extraHeaders { | ||||
| 		b, err := json.Marshal(v) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		(*obj.protected)[k] = makeRawMessage(b) | ||||
| 	} | ||||
| 
 | ||||
| 	authData := obj.computeAuthData() | ||||
| 	parts, err := ctx.cipher.encrypt(cek, authData, plaintext) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	obj.iv = parts.iv | ||||
| 	obj.ciphertext = parts.ciphertext | ||||
| 	obj.tag = parts.tag | ||||
| 
 | ||||
| 	return obj, nil | ||||
| } | ||||
| 
 | ||||
| func (ctx *genericEncrypter) Options() EncrypterOptions { | ||||
| 	return EncrypterOptions{ | ||||
| 		Compression:  ctx.compressionAlg, | ||||
| 		ExtraHeaders: ctx.extraHeaders, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Decrypt and validate the object and return the plaintext. Note that this
 | ||||
| // function does not support multi-recipient, if you desire multi-recipient
 | ||||
| // decryption use DecryptMulti instead.
 | ||||
| func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { | ||||
| 	headers := obj.mergedHeaders(nil) | ||||
| 
 | ||||
| 	if len(obj.recipients) > 1 { | ||||
| 		return nil, errors.New("square/go-jose: too many recipients in payload; expecting only one") | ||||
| 	} | ||||
| 
 | ||||
| 	critical, err := headers.getCritical() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("square/go-jose: invalid crit header") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(critical) > 0 { | ||||
| 		return nil, fmt.Errorf("square/go-jose: unsupported crit header") | ||||
| 	} | ||||
| 
 | ||||
| 	decrypter, err := newDecrypter(decryptionKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	cipher := getContentCipher(headers.getEncryption()) | ||||
| 	if cipher == nil { | ||||
| 		return nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(headers.getEncryption())) | ||||
| 	} | ||||
| 
 | ||||
| 	generator := randomKeyGenerator{ | ||||
| 		size: cipher.keySize(), | ||||
| 	} | ||||
| 
 | ||||
| 	parts := &aeadParts{ | ||||
| 		iv:         obj.iv, | ||||
| 		ciphertext: obj.ciphertext, | ||||
| 		tag:        obj.tag, | ||||
| 	} | ||||
| 
 | ||||
| 	authData := obj.computeAuthData() | ||||
| 
 | ||||
| 	var plaintext []byte | ||||
| 	recipient := obj.recipients[0] | ||||
| 	recipientHeaders := obj.mergedHeaders(&recipient) | ||||
| 
 | ||||
| 	cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) | ||||
| 	if err == nil { | ||||
| 		// Found a valid CEK -- let's try to decrypt.
 | ||||
| 		plaintext, err = cipher.decrypt(cek, authData, parts) | ||||
| 	} | ||||
| 
 | ||||
| 	if plaintext == nil { | ||||
| 		return nil, ErrCryptoFailure | ||||
| 	} | ||||
| 
 | ||||
| 	// The "zip" header parameter may only be present in the protected header.
 | ||||
| 	if comp := obj.protected.getCompression(); comp != "" { | ||||
| 		plaintext, err = decompress(comp, plaintext) | ||||
| 	} | ||||
| 
 | ||||
| 	return plaintext, err | ||||
| } | ||||
| 
 | ||||
| // DecryptMulti decrypts and validates the object and returns the plaintexts,
 | ||||
| // with support for multiple recipients. It returns the index of the recipient
 | ||||
| // for which the decryption was successful, the merged headers for that recipient,
 | ||||
| // and the plaintext.
 | ||||
| func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { | ||||
| 	globalHeaders := obj.mergedHeaders(nil) | ||||
| 
 | ||||
| 	critical, err := globalHeaders.getCritical() | ||||
| 	if err != nil { | ||||
| 		return -1, Header{}, nil, fmt.Errorf("square/go-jose: invalid crit header") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(critical) > 0 { | ||||
| 		return -1, Header{}, nil, fmt.Errorf("square/go-jose: unsupported crit header") | ||||
| 	} | ||||
| 
 | ||||
| 	decrypter, err := newDecrypter(decryptionKey) | ||||
| 	if err != nil { | ||||
| 		return -1, Header{}, nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	encryption := globalHeaders.getEncryption() | ||||
| 	cipher := getContentCipher(encryption) | ||||
| 	if cipher == nil { | ||||
| 		return -1, Header{}, nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(encryption)) | ||||
| 	} | ||||
| 
 | ||||
| 	generator := randomKeyGenerator{ | ||||
| 		size: cipher.keySize(), | ||||
| 	} | ||||
| 
 | ||||
| 	parts := &aeadParts{ | ||||
| 		iv:         obj.iv, | ||||
| 		ciphertext: obj.ciphertext, | ||||
| 		tag:        obj.tag, | ||||
| 	} | ||||
| 
 | ||||
| 	authData := obj.computeAuthData() | ||||
| 
 | ||||
| 	index := -1 | ||||
| 	var plaintext []byte | ||||
| 	var headers rawHeader | ||||
| 
 | ||||
| 	for i, recipient := range obj.recipients { | ||||
| 		recipientHeaders := obj.mergedHeaders(&recipient) | ||||
| 
 | ||||
| 		cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) | ||||
| 		if err == nil { | ||||
| 			// Found a valid CEK -- let's try to decrypt.
 | ||||
| 			plaintext, err = cipher.decrypt(cek, authData, parts) | ||||
| 			if err == nil { | ||||
| 				index = i | ||||
| 				headers = recipientHeaders | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if plaintext == nil || err != nil { | ||||
| 		return -1, Header{}, nil, ErrCryptoFailure | ||||
| 	} | ||||
| 
 | ||||
| 	// The "zip" header parameter may only be present in the protected header.
 | ||||
| 	if comp := obj.protected.getCompression(); comp != "" { | ||||
| 		plaintext, err = decompress(comp, plaintext) | ||||
| 	} | ||||
| 
 | ||||
| 	sanitized, err := headers.sanitized() | ||||
| 	if err != nil { | ||||
| 		return -1, Header{}, nil, fmt.Errorf("square/go-jose: failed to sanitize header: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return index, sanitized, plaintext, err | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue