Merge pull request #696 from ericchiang/switch-go-oidc-client

*: switch oidc client to github.com/coreos/go-oidc
This commit is contained in:
Eric Chiang 2016-11-22 13:42:28 -08:00 committed by GitHub
commit a607ff7a3a
196 changed files with 12044 additions and 18663 deletions

View file

@ -17,7 +17,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ericchiang/oidc" "github.com/coreos/go-oidc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -173,7 +173,7 @@ func cmd() *cobra.Command {
} }
a.provider = provider a.provider = provider
a.verifier = provider.NewVerifier(a.ctx, oidc.VerifyAudience(a.clientID)) a.verifier = provider.Verifier(oidc.VerifyAudience(a.clientID))
http.HandleFunc("/", a.handleIndex) http.HandleFunc("/", a.handleIndex)
http.HandleFunc("/login", a.handleLogin) http.HandleFunc("/login", a.handleLogin)
@ -269,7 +269,7 @@ func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) {
RefreshToken: refresh, RefreshToken: refresh,
Expiry: time.Now().Add(-time.Hour), Expiry: time.Now().Add(-time.Hour),
} }
token, err = oauth2Config.TokenSource(a.ctx, t).Token() token, err = oauth2Config.TokenSource(r.Context(), t).Token()
default: default:
http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest) http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
return return
@ -286,7 +286,7 @@ func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) {
return return
} }
idToken, err := a.verifier.Verify(rawIDToken) idToken, err := a.verifier.Verify(r.Context(), rawIDToken)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Failed to verify ID token: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Failed to verify ID token: %v", err), http.StatusInternalServerError)
return return

View file

@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/ericchiang/oidc" "github.com/coreos/go-oidc"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -51,7 +51,7 @@ func (c *Config) Open() (conn connector.Connector, err error) {
Scopes: scopes, Scopes: scopes,
RedirectURL: c.RedirectURI, RedirectURL: c.RedirectURI,
}, },
verifier: provider.NewVerifier(ctx, verifier: provider.Verifier(
oidc.VerifyExpiry(), oidc.VerifyExpiry(),
oidc.VerifyAudience(clientID), oidc.VerifyAudience(clientID),
), ),
@ -99,7 +99,7 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (ide
if errType := q.Get("error"); errType != "" { if errType := q.Get("error"); errType != "" {
return identity, &oauth2Error{errType, q.Get("error_description")} return identity, &oauth2Error{errType, q.Get("error_description")}
} }
token, err := c.oauth2Config.Exchange(c.ctx, q.Get("code")) token, err := c.oauth2Config.Exchange(r.Context(), q.Get("code"))
if err != nil { if err != nil {
return identity, fmt.Errorf("oidc: failed to get token: %v", err) return identity, fmt.Errorf("oidc: failed to get token: %v", err)
} }
@ -108,7 +108,7 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (ide
if !ok { if !ok {
return identity, errors.New("oidc: no id_token in token response") return identity, errors.New("oidc: no id_token in token response")
} }
idToken, err := c.verifier.Verify(rawIDToken) idToken, err := c.verifier.Verify(r.Context(), rawIDToken)
if err != nil { if err != nil {
return identity, fmt.Errorf("oidc: failed to verify ID Token: %v", err) return identity, fmt.Errorf("oidc: failed to verify ID Token: %v", err)
} }

16
glide.lock generated
View file

@ -1,12 +1,12 @@
hash: bc7fa6bbfddcb39710064a9d6dab090d49bd88486571cae12c68c4b70093e716 hash: c3530f2a60a64c2efc4c3ac499fcd15f79de2a532715ba2b9841c1d404942b2e
updated: 2016-11-03T14:31:37.323302694-07:00 updated: 2016-11-17T15:18:56.701287533-08:00
imports: imports:
- name: github.com/cockroachdb/cockroach-go - name: github.com/cockroachdb/cockroach-go
version: 31611c0501c812f437d4861d87d117053967c955 version: 31611c0501c812f437d4861d87d117053967c955
subpackages: subpackages:
- crdb - crdb
- name: github.com/ericchiang/oidc - name: github.com/coreos/go-oidc
version: 1907f0e61549f9081f26bdf269f11603496c9dee version: 5a7f09ab5787e846efa7f56f4a08b6d6926d08c4
- name: github.com/ghodss/yaml - name: github.com/ghodss/yaml
version: bea76d6a4713e18b7f5321a2b020738552def3ea version: bea76d6a4713e18b7f5321a2b020738552def3ea
- name: github.com/go-sql-driver/mysql - name: github.com/go-sql-driver/mysql
@ -52,6 +52,7 @@ imports:
version: 6a513affb38dc9788b449d59ffed099b8de18fa0 version: 6a513affb38dc9788b449d59ffed099b8de18fa0
subpackages: subpackages:
- context - context
- context/ctxhttp
- http2 - http2
- http2/hpack - http2/hpack
- internal/timeseries - internal/timeseries
@ -87,13 +88,8 @@ imports:
version: 4e86f4367175e39f69d9358a5f17b4dda270378d version: 4e86f4367175e39f69d9358a5f17b4dda270378d
- name: gopkg.in/ldap.v2 - name: gopkg.in/ldap.v2
version: 0e7db8eb77695b5a952f0e5d78df9ab160050c73 version: 0e7db8eb77695b5a952f0e5d78df9ab160050c73
- name: gopkg.in/square/go-jose.v1
version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc
subpackages:
- cipher
- json
- name: gopkg.in/square/go-jose.v2 - name: gopkg.in/square/go-jose.v2
version: f209f41628247c56938cb20ef51d589ddad6c30b version: 8c5257b2f658f86d174ae68c6a592eaf6a9608d9
subpackages: subpackages:
- cipher - cipher
- json - json

View file

@ -17,7 +17,7 @@ import:
version: 4e86f4367175e39f69d9358a5f17b4dda270378d version: 4e86f4367175e39f69d9358a5f17b4dda270378d
- package: gopkg.in/square/go-jose.v2 - package: gopkg.in/square/go-jose.v2
version: f209f41628247c56938cb20ef51d589ddad6c30b version: v2.0.0
subpackages: subpackages:
- cipher - cipher
- json - json
@ -26,6 +26,7 @@ import:
version: 6a513affb38dc9788b449d59ffed099b8de18fa0 version: 6a513affb38dc9788b449d59ffed099b8de18fa0
subpackages: subpackages:
- context - context
- context/ctxhttp
- http2 - http2
- http2/hpack - http2/hpack
- internal/timeseries - internal/timeseries
@ -49,16 +50,17 @@ import:
subpackages: subpackages:
- bcrypt - bcrypt
- package: github.com/ericchiang/oidc - package: github.com/coreos/go-oidc
version: 1907f0e61549f9081f26bdf269f11603496c9dee version: 5a7f09ab5787e846efa7f56f4a08b6d6926d08c4
- package: github.com/pquerna/cachecontrol - package: github.com/pquerna/cachecontrol
version: c97913dcbd76de40b051a9b4cd827f7eaeb7a868 version: c97913dcbd76de40b051a9b4cd827f7eaeb7a868
- package: gopkg.in/square/go-jose.v1
version: v1.0.2
- package: golang.org/x/oauth2 - package: golang.org/x/oauth2
version: 08c8d727d2392d18286f9f88ad775ad98f09ab33 version: 08c8d727d2392d18286f9f88ad775ad98f09ab33
# Not actually imported but glide detects it. Consider adding subpackages to subpackages: []
# the oauth2 package to eliminate. # The oauth2 package only imports the appengine code when it's given a
# specific build tags, but glide detects it anyway.
#
# https://github.com/golang/oauth2/blob/d5040cdd/client_appengine.go
- package: google.golang.org/appengine - package: google.golang.org/appengine
version: 267c27e7492265b84fc6719503b14a1e17975d79 version: 267c27e7492265b84fc6719503b14a1e17975d79
subpackages: subpackages:

View file

@ -3,6 +3,7 @@ package server
import ( import (
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/json"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
@ -17,7 +18,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ericchiang/oidc" oidc "github.com/coreos/go-oidc"
"github.com/kylelemons/godebug/pretty" "github.com/kylelemons/godebug/pretty"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -117,17 +118,21 @@ func TestDiscovery(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to get provider: %v", err) t.Fatalf("failed to get provider: %v", err)
} }
required := []struct {
name, val string var got map[string]*json.RawMessage
}{ if err := p.Claims(&got); err != nil {
{"issuer", p.Issuer}, t.Fatalf("failed to decode claims: %v", err)
{"authorization_endpoint", p.AuthURL}, }
{"token_endpoint", p.TokenURL},
{"jwks_uri", p.JWKSURL}, required := []string{
"issuer",
"authorization_endpoint",
"token_endpoint",
"jwks_uri",
} }
for _, field := range required { for _, field := range required {
if field.val == "" { if _, ok := got[field]; !ok {
t.Errorf("server discovery is missing required field %q", field.name) t.Errorf("server discovery is missing required field %q", field)
} }
} }
} }
@ -169,7 +174,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
if !ok { if !ok {
return fmt.Errorf("no id token found") return fmt.Errorf("no id token found")
} }
if _, err := p.NewVerifier(ctx).Verify(idToken); err != nil { if _, err := p.Verifier().Verify(ctx, idToken); err != nil {
return fmt.Errorf("failed to verify id token: %v", err) return fmt.Errorf("failed to verify id token: %v", err)
} }
return nil return nil
@ -192,7 +197,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
if !ok { if !ok {
return fmt.Errorf("no id token found") return fmt.Errorf("no id token found")
} }
idToken, err := p.NewVerifier(ctx).Verify(rawIDToken) idToken, err := p.Verifier().Verify(ctx, rawIDToken)
if err != nil { if err != nil {
return fmt.Errorf("failed to verify id token: %v", err) return fmt.Errorf("failed to verify id token: %v", err)
} }
@ -230,7 +235,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
v.Add("grant_type", "refresh_token") v.Add("grant_type", "refresh_token")
v.Add("refresh_token", token.RefreshToken) v.Add("refresh_token", token.RefreshToken)
v.Add("scope", strings.Join(requestedScopes, " ")) v.Add("scope", strings.Join(requestedScopes, " "))
resp, err := http.PostForm(p.TokenURL, v) resp, err := http.PostForm(p.Endpoint().TokenURL, v)
if err != nil { if err != nil {
return err return err
} }
@ -258,7 +263,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
// Since we support that client we choose to be more relaxed about // Since we support that client we choose to be more relaxed about
// scope parsing, disregarding extra whitespace. // scope parsing, disregarding extra whitespace.
v.Add("scope", " "+strings.Join(requestedScopes, " ")) v.Add("scope", " "+strings.Join(requestedScopes, " "))
resp, err := http.PostForm(p.TokenURL, v) resp, err := http.PostForm(p.Endpoint().TokenURL, v)
if err != nil { if err != nil {
return err return err
} }
@ -284,7 +289,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
v.Add("refresh_token", token.RefreshToken) v.Add("refresh_token", token.RefreshToken)
// Request a scope that wasn't requestd initially. // Request a scope that wasn't requestd initially.
v.Add("scope", "oidc email profile") v.Add("scope", "oidc email profile")
resp, err := http.PostForm(p.TokenURL, v) resp, err := http.PostForm(p.Endpoint().TokenURL, v)
if err != nil { if err != nil {
return err return err
} }
@ -335,7 +340,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
if !ok { if !ok {
return fmt.Errorf("no id_token in refreshed token") return fmt.Errorf("no id_token in refreshed token")
} }
idToken, err := p.NewVerifier(ctx).Verify(rawIDToken) idToken, err := p.Verifier().Verify(ctx, rawIDToken)
if err != nil { if err != nil {
return fmt.Errorf("failed to verify id token: %v", err) return fmt.Errorf("failed to verify id token: %v", err)
} }
@ -547,7 +552,7 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
src := &nonceSource{nonce: nonce} src := &nonceSource{nonce: nonce}
idTokenVerifier := p.NewVerifier(ctx, oidc.VerifyAudience(client.ID), oidc.VerifyNonce(src)) idTokenVerifier := p.Verifier(oidc.VerifyAudience(client.ID), oidc.VerifyNonce(src))
oauth2Config = &oauth2.Config{ oauth2Config = &oauth2.Config{
ClientID: client.ID, ClientID: client.ID,
@ -569,7 +574,7 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
if idToken == "" { if idToken == "" {
return errors.New("no id_token in fragment") return errors.New("no id_token in fragment")
} }
if _, err := idTokenVerifier.Verify(idToken); err != nil { if _, err := idTokenVerifier.Verify(ctx, idToken); err != nil {
return fmt.Errorf("failed to verify id_token: %v", err) return fmt.Errorf("failed to verify id_token: %v", err)
} }
return nil return nil
@ -664,7 +669,7 @@ func TestCrossClientScopes(t *testing.T) {
t.Errorf("no id token found: %v", err) t.Errorf("no id token found: %v", err)
return return
} }
idToken, err := p.NewVerifier(ctx).Verify(rawIDToken) idToken, err := p.Verifier().Verify(ctx, rawIDToken)
if err != nil { if err != nil {
t.Errorf("failed to parse ID Token: %v", err) t.Errorf("failed to parse ID Token: %v", err)
return return

2
vendor/github.com/coreos/go-oidc/.gitignore generated vendored Normal file
View file

@ -0,0 +1,2 @@
/bin
/gopath

16
vendor/github.com/coreos/go-oidc/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,16 @@
language: go
go:
- 1.7.3
- 1.6.3
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

71
vendor/github.com/coreos/go-oidc/CONTRIBUTING.md generated vendored Normal file
View file

@ -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.

36
vendor/github.com/coreos/go-oidc/DCO generated vendored Normal file
View file

@ -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.

3
vendor/github.com/coreos/go-oidc/MAINTAINERS generated vendored Normal file
View file

@ -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)

5
vendor/github.com/coreos/go-oidc/NOTICE generated vendored Normal file
View file

@ -0,0 +1,5 @@
CoreOS Project
Copyright 2014 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

72
vendor/github.com/coreos/go-oidc/README.md generated vendored Normal file
View file

@ -0,0 +1,72 @@
# go-oidc
[![GoDoc](https://godoc.org/github.com/coreos/go-oidc?status.svg)](https://godoc.org/github.com/coreos/go-oidc)
[![Build Status](https://travis-ci.org/coreos/go-oidc.png?branch=master)](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()
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
}
}
```

21
vendor/github.com/coreos/go-oidc/example/README.md generated vendored Normal file
View file

@ -0,0 +1,21 @@
# Examples
These are example uses of the oidc package. Each requires a Google account and the client ID and secret of a registered OAuth2 application. To create one:
1. Visit your [Google Developer Console][google-developer-console].
2. Click "Credentials" on the left column.
3. Click the "Create credentials" button followed by "OAuth client ID".
4. Select "Web application" and add "http://127.0.0.1:5556/auth/google/callback" as an authorized redirect URI.
5. Click create and add the printed client ID and secret to your environment using the following variables:
```
GOOGLE_OAUTH2_CLIENT_ID
GOOGLE_OAUTH2_CLIENT_SECRET
```
Finally run the examples using the Go tool and navigate to http://127.0.0.1:5556.
```
go run ./examples/idtoken/app.go
```
[google-developer-console]: https://console.developers.google.com/apis/dashboard

View file

@ -9,7 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/ericchiang/oidc" oidc "github.com/coreos/go-oidc"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -27,7 +27,7 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
verifier := provider.NewVerifier(ctx) verifier := provider.Verifier()
config := oauth2.Config{ config := oauth2.Config{
ClientID: clientID, ClientID: clientID,
@ -59,7 +59,7 @@ func main() {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError) http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
return return
} }
idToken, err := verifier.Verify(rawIDToken) idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil { if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return return

View file

@ -10,7 +10,7 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/ericchiang/oidc" oidc "github.com/coreos/go-oidc"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -42,7 +42,7 @@ func main() {
} }
// Use the nonce source to create a custom ID Token verifier. // Use the nonce source to create a custom ID Token verifier.
nonceEnabledVerifier := provider.NewVerifier(ctx, oidc.VerifyNonce(nonceSource{})) nonceEnabledVerifier := provider.Verifier(oidc.VerifyNonce(nonceSource{}))
config := oauth2.Config{ config := oauth2.Config{
ClientID: clientID, ClientID: clientID,
@ -76,7 +76,7 @@ func main() {
return return
} }
// Verify the ID Token signature and nonce. // Verify the ID Token signature and nonce.
idToken, err := nonceEnabledVerifier.Verify(rawIDToken) idToken, err := nonceEnabledVerifier.Verify(ctx, rawIDToken)
if err != nil { if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return return

View file

@ -9,7 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/ericchiang/oidc" oidc "github.com/coreos/go-oidc"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"

150
vendor/github.com/coreos/go-oidc/gen.go generated vendored Normal file
View file

@ -0,0 +1,150 @@
// +build ignore
// This file is used to generate keys for tests.
package main
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"text/template"
jose "gopkg.in/square/go-jose.v2"
)
type key struct {
name string
new func() (crypto.Signer, error)
}
var keys = []key{
{
"ECDSA_256", func() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
},
},
{
"ECDSA_384", func() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
},
},
{
"ECDSA_521", func() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
},
},
{
"RSA_1024", func() (crypto.Signer, error) {
return rsa.GenerateKey(rand.Reader, 1024)
},
},
{
"RSA_2048", func() (crypto.Signer, error) {
return rsa.GenerateKey(rand.Reader, 2048)
},
},
{
"RSA_4096", func() (crypto.Signer, error) {
return rsa.GenerateKey(rand.Reader, 4096)
},
},
}
func newJWK(k key, prefix, ident string) (privBytes, pubBytes []byte, err error) {
priv, err := k.new()
if err != nil {
return nil, nil, fmt.Errorf("generate %s: %v", k.name, err)
}
pub := priv.Public()
privKey := &jose.JSONWebKey{Key: priv}
thumbprint, err := privKey.Thumbprint(crypto.SHA256)
if err != nil {
return nil, nil, fmt.Errorf("computing thumbprint: %v", err)
}
keyID := hex.EncodeToString(thumbprint)
privKey.KeyID = keyID
pubKey := &jose.JSONWebKey{Key: pub, KeyID: keyID}
privBytes, err = json.MarshalIndent(privKey, prefix, ident)
if err != nil {
return
}
pubBytes, err = json.MarshalIndent(pubKey, prefix, ident)
return
}
type keyData struct {
Name string
Priv string
Pub string
}
var tmpl = template.Must(template.New("").Parse(`// +build !golint
// This file contains statically created JWKs for tests created by gen.go
package oidc
import (
"encoding/json"
jose "gopkg.in/square/go-jose.v2"
)
func mustLoadJWK(s string) jose.JSONWebKey {
var jwk jose.JSONWebKey
if err := json.Unmarshal([]byte(s), &jwk); err != nil {
panic(err)
}
return jwk
}
var (
{{- range $i, $key := .Keys }}
testKey{{ $key.Name }} = mustLoadJWK(` + "`" + `{{ $key.Pub }}` + "`" + `)
testKey{{ $key.Name }}_Priv = mustLoadJWK(` + "`" + `{{ $key.Priv }}` + "`" + `)
{{ end -}}
)
`))
func main() {
var tmplData struct {
Keys []keyData
}
for _, k := range keys {
for i := 0; i < 4; i++ {
log.Printf("generating %s", k.name)
priv, pub, err := newJWK(k, "\t", "\t")
if err != nil {
log.Fatal(err)
}
name := fmt.Sprintf("%s_%d", k.name, i)
tmplData.Keys = append(tmplData.Keys, keyData{
Name: name,
Priv: string(priv),
Pub: string(pub),
})
}
}
buff := new(bytes.Buffer)
if err := tmpl.Execute(buff, tmplData); err != nil {
log.Fatalf("excuting template: %v", err)
}
if err := ioutil.WriteFile("jose_test.go", buff.Bytes(), 0644); err != nil {
log.Fatal(err)
}
}

7
vendor/github.com/coreos/go-oidc/http/client.go generated vendored Normal file
View file

@ -0,0 +1,7 @@
package http
import "net/http"
type Client interface {
Do(*http.Request) (*http.Response, error)
}

2
vendor/github.com/coreos/go-oidc/http/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package http is DEPRECATED. Use net/http instead.
package http

156
vendor/github.com/coreos/go-oidc/http/http.go generated vendored Normal file
View file

@ -0,0 +1,156 @@
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
}
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
}

380
vendor/github.com/coreos/go-oidc/http/http_test.go generated vendored Normal file
View file

@ -0,0 +1,380 @@
package http
import (
"net/http"
"net/url"
"reflect"
"strings"
"testing"
"time"
)
func TestCacheControlMaxAgeSuccess(t *testing.T) {
tests := []struct {
hdr string
wantAge time.Duration
wantOK bool
}{
{"max-age=12", 12 * time.Second, true},
{"max-age=-12", 0, false},
{"max-age=0", 0, false},
{"public, max-age=12", 12 * time.Second, true},
{"public, max-age=40192, must-revalidate", 40192 * time.Second, true},
{"public, not-max-age=12, must-revalidate", time.Duration(0), false},
}
for i, tt := range tests {
maxAge, ok, err := cacheControlMaxAge(tt.hdr)
if err != nil {
t.Errorf("case %d: err=%v", i, err)
}
if tt.wantAge != maxAge {
t.Errorf("case %d: want=%d got=%d", i, tt.wantAge, maxAge)
}
if tt.wantOK != ok {
t.Errorf("case %d: incorrect ok value: want=%t got=%t", i, tt.wantOK, ok)
}
}
}
func TestCacheControlMaxAgeFail(t *testing.T) {
tests := []string{
"max-age=aasdf",
"max-age=",
"max-age",
}
for i, tt := range tests {
_, ok, err := cacheControlMaxAge(tt)
if ok {
t.Errorf("case %d: want ok=false, got true", i)
}
if err == nil {
t.Errorf("case %d: want non-nil err", i)
}
}
}
func TestMergeQuery(t *testing.T) {
tests := []struct {
u string
q url.Values
w string
}{
// No values
{
u: "http://example.com",
q: nil,
w: "http://example.com",
},
// No additional values
{
u: "http://example.com?foo=bar",
q: nil,
w: "http://example.com?foo=bar",
},
// Simple addition
{
u: "http://example.com",
q: url.Values{
"foo": []string{"bar"},
},
w: "http://example.com?foo=bar",
},
// Addition with existing values
{
u: "http://example.com?dog=boo",
q: url.Values{
"foo": []string{"bar"},
},
w: "http://example.com?dog=boo&foo=bar",
},
// Merge
{
u: "http://example.com?dog=boo",
q: url.Values{
"dog": []string{"elroy"},
},
w: "http://example.com?dog=boo&dog=elroy",
},
// Add and merge
{
u: "http://example.com?dog=boo",
q: url.Values{
"dog": []string{"elroy"},
"foo": []string{"bar"},
},
w: "http://example.com?dog=boo&dog=elroy&foo=bar",
},
// Multivalue merge
{
u: "http://example.com?dog=boo",
q: url.Values{
"dog": []string{"elroy", "penny"},
},
w: "http://example.com?dog=boo&dog=elroy&dog=penny",
},
}
for i, tt := range tests {
ur, err := url.Parse(tt.u)
if err != nil {
t.Errorf("case %d: failed parsing test url: %v, error: %v", i, tt.u, err)
}
got := MergeQuery(*ur, tt.q)
want, err := url.Parse(tt.w)
if err != nil {
t.Errorf("case %d: failed parsing want url: %v, error: %v", i, tt.w, err)
}
if !reflect.DeepEqual(*want, got) {
t.Errorf("case %d: want: %v, got: %v", i, *want, got)
}
}
}
func TestExpiresPass(t *testing.T) {
tests := []struct {
date string
exp string
wantTTL time.Duration
wantOK bool
}{
// Expires and Date properly set
{
date: "Thu, 01 Dec 1983 22:00:00 GMT",
exp: "Fri, 02 Dec 1983 01:00:00 GMT",
wantTTL: 10800 * time.Second,
wantOK: true,
},
// empty headers
{
date: "",
exp: "",
wantOK: false,
},
// lack of Expirs short-ciruits Date parsing
{
date: "foo",
exp: "",
wantOK: false,
},
// lack of Date short-ciruits Expires parsing
{
date: "",
exp: "foo",
wantOK: false,
},
// no Date
{
exp: "Thu, 01 Dec 1983 22:00:00 GMT",
wantTTL: 0,
wantOK: false,
},
// no Expires
{
date: "Thu, 01 Dec 1983 22:00:00 GMT",
wantTTL: 0,
wantOK: false,
},
// Expires < Date
{
date: "Fri, 02 Dec 1983 01:00:00 GMT",
exp: "Thu, 01 Dec 1983 22:00:00 GMT",
wantTTL: 0,
wantOK: false,
},
}
for i, tt := range tests {
ttl, ok, err := expires(tt.date, tt.exp)
if err != nil {
t.Errorf("case %d: err=%v", i, err)
}
if tt.wantTTL != ttl {
t.Errorf("case %d: want=%d got=%d", i, tt.wantTTL, ttl)
}
if tt.wantOK != ok {
t.Errorf("case %d: incorrect ok value: want=%t got=%t", i, tt.wantOK, ok)
}
}
}
func TestExpiresFail(t *testing.T) {
tests := []struct {
date string
exp string
}{
// malformed Date header
{
date: "foo",
exp: "Fri, 02 Dec 1983 01:00:00 GMT",
},
// malformed exp header
{
date: "Fri, 02 Dec 1983 01:00:00 GMT",
exp: "bar",
},
}
for i, tt := range tests {
_, _, err := expires(tt.date, tt.exp)
if err == nil {
t.Errorf("case %d: expected non-nil error", i)
}
}
}
func TestCacheablePass(t *testing.T) {
tests := []struct {
headers http.Header
wantTTL time.Duration
wantOK bool
}{
// valid Cache-Control
{
headers: http.Header{
"Cache-Control": []string{"max-age=100"},
},
wantTTL: 100 * time.Second,
wantOK: true,
},
// valid Date/Expires
{
headers: http.Header{
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
"Expires": []string{"Fri, 02 Dec 1983 01:00:00 GMT"},
},
wantTTL: 10800 * time.Second,
wantOK: true,
},
// Cache-Control supersedes Date/Expires
{
headers: http.Header{
"Cache-Control": []string{"max-age=100"},
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
"Expires": []string{"Fri, 02 Dec 1983 01:00:00 GMT"},
},
wantTTL: 100 * time.Second,
wantOK: true,
},
// no caching headers
{
headers: http.Header{},
wantOK: false,
},
}
for i, tt := range tests {
ttl, ok, err := Cacheable(tt.headers)
if err != nil {
t.Errorf("case %d: err=%v", i, err)
continue
}
if tt.wantTTL != ttl {
t.Errorf("case %d: want=%d got=%d", i, tt.wantTTL, ttl)
}
if tt.wantOK != ok {
t.Errorf("case %d: incorrect ok value: want=%t got=%t", i, tt.wantOK, ok)
}
}
}
func TestCacheableFail(t *testing.T) {
tests := []http.Header{
// invalid Cache-Control short-circuits
http.Header{
"Cache-Control": []string{"max-age"},
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
"Expires": []string{"Fri, 02 Dec 1983 01:00:00 GMT"},
},
// no Cache-Control, invalid Expires
http.Header{
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
"Expires": []string{"boo"},
},
}
for i, tt := range tests {
_, _, err := Cacheable(tt)
if err == nil {
t.Errorf("case %d: want non-nil err", i)
}
}
}
func TestNewResourceLocation(t *testing.T) {
tests := []struct {
ru *url.URL
id string
want string
}{
{
ru: &url.URL{
Scheme: "http",
Host: "example.com",
},
id: "foo",
want: "http://example.com/foo",
},
// https
{
ru: &url.URL{
Scheme: "https",
Host: "example.com",
},
id: "foo",
want: "https://example.com/foo",
},
// with path
{
ru: &url.URL{
Scheme: "http",
Host: "example.com",
Path: "one/two/three",
},
id: "foo",
want: "http://example.com/one/two/three/foo",
},
// with fragment
{
ru: &url.URL{
Scheme: "http",
Host: "example.com",
Fragment: "frag",
},
id: "foo",
want: "http://example.com/foo",
},
// with query
{
ru: &url.URL{
Scheme: "http",
Host: "example.com",
RawQuery: "dog=elroy",
},
id: "foo",
want: "http://example.com/foo",
},
}
for i, tt := range tests {
got := NewResourceLocation(tt.ru, tt.id)
if tt.want != got {
t.Errorf("case %d: want=%s, got=%s", i, tt.want, got)
}
}
}
func TestCopyRequest(t *testing.T) {
r1, err := http.NewRequest("GET", "http://example.com", strings.NewReader("foo"))
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
r2 := CopyRequest(r1)
if !reflect.DeepEqual(r1, r2) {
t.Fatalf("Result of CopyRequest incorrect: %#v != %#v", r1, r2)
}
}

29
vendor/github.com/coreos/go-oidc/http/url.go generated vendored Normal file
View file

@ -0,0 +1,29 @@
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
}

49
vendor/github.com/coreos/go-oidc/http/url_test.go generated vendored Normal file
View file

@ -0,0 +1,49 @@
package http
import (
"net/url"
"testing"
)
func TestParseNonEmptyURL(t *testing.T) {
tests := []struct {
u string
ok bool
}{
{"", false},
{"http://", false},
{"example.com", false},
{"example", false},
{"http://example", true},
{"http://example:1234", true},
{"http://example.com", true},
{"http://example.com:1234", true},
}
for i, tt := range tests {
u, err := ParseNonEmptyURL(tt.u)
if err != nil {
t.Logf("err: %v", err)
if tt.ok {
t.Errorf("case %d: unexpected error: %v", i, err)
} else {
continue
}
}
if !tt.ok {
t.Errorf("case %d: expected error but got none", i)
continue
}
uu, err := url.Parse(tt.u)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
if uu.String() != u.String() {
t.Errorf("case %d: incorrect url value, want: %q, got: %q", i, uu.String(), u.String())
}
}
}

20
vendor/github.com/coreos/go-oidc/jose.go generated vendored Normal file
View file

@ -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
)

126
vendor/github.com/coreos/go-oidc/jose/claims.go generated vendored Normal file
View file

@ -0,0 +1,126 @@
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
}

328
vendor/github.com/coreos/go-oidc/jose/claims_test.go generated vendored Normal file
View file

@ -0,0 +1,328 @@
package jose
import (
"reflect"
"testing"
"time"
)
func TestString(t *testing.T) {
tests := []struct {
cl Claims
key string
ok bool
err bool
val string
}{
// ok, no err, claim exists
{
cl: Claims{
"foo": "bar",
},
key: "foo",
val: "bar",
ok: true,
err: false,
},
// no claims
{
cl: Claims{},
key: "foo",
val: "",
ok: false,
err: false,
},
// missing claim
{
cl: Claims{
"foo": "bar",
},
key: "xxx",
val: "",
ok: false,
err: false,
},
// unparsable: type
{
cl: Claims{
"foo": struct{}{},
},
key: "foo",
val: "",
ok: false,
err: true,
},
// unparsable: nil value
{
cl: Claims{
"foo": nil,
},
key: "foo",
val: "",
ok: false,
err: true,
},
}
for i, tt := range tests {
val, ok, err := tt.cl.StringClaim(tt.key)
if tt.err && err == nil {
t.Errorf("case %d: want err=non-nil, got err=nil", i)
} else if !tt.err && err != nil {
t.Errorf("case %d: want err=nil, got err=%v", i, err)
}
if tt.ok != ok {
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
}
if tt.val != val {
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
}
}
}
func TestInt64(t *testing.T) {
tests := []struct {
cl Claims
key string
ok bool
err bool
val int64
}{
// ok, no err, claim exists
{
cl: Claims{
"foo": int64(100),
},
key: "foo",
val: int64(100),
ok: true,
err: false,
},
// no claims
{
cl: Claims{},
key: "foo",
val: 0,
ok: false,
err: false,
},
// missing claim
{
cl: Claims{
"foo": "bar",
},
key: "xxx",
val: 0,
ok: false,
err: false,
},
// unparsable: type
{
cl: Claims{
"foo": struct{}{},
},
key: "foo",
val: 0,
ok: false,
err: true,
},
// unparsable: nil value
{
cl: Claims{
"foo": nil,
},
key: "foo",
val: 0,
ok: false,
err: true,
},
}
for i, tt := range tests {
val, ok, err := tt.cl.Int64Claim(tt.key)
if tt.err && err == nil {
t.Errorf("case %d: want err=non-nil, got err=nil", i)
} else if !tt.err && err != nil {
t.Errorf("case %d: want err=nil, got err=%v", i, err)
}
if tt.ok != ok {
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
}
if tt.val != val {
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
}
}
}
func TestTime(t *testing.T) {
now := time.Now().UTC()
unixNow := now.Unix()
tests := []struct {
cl Claims
key string
ok bool
err bool
val time.Time
}{
// ok, no err, claim exists
{
cl: Claims{
"foo": unixNow,
},
key: "foo",
val: time.Unix(now.Unix(), 0).UTC(),
ok: true,
err: false,
},
// no claims
{
cl: Claims{},
key: "foo",
val: time.Time{},
ok: false,
err: false,
},
// missing claim
{
cl: Claims{
"foo": "bar",
},
key: "xxx",
val: time.Time{},
ok: false,
err: false,
},
// unparsable: type
{
cl: Claims{
"foo": struct{}{},
},
key: "foo",
val: time.Time{},
ok: false,
err: true,
},
// unparsable: nil value
{
cl: Claims{
"foo": nil,
},
key: "foo",
val: time.Time{},
ok: false,
err: true,
},
}
for i, tt := range tests {
val, ok, err := tt.cl.TimeClaim(tt.key)
if tt.err && err == nil {
t.Errorf("case %d: want err=non-nil, got err=nil", i)
} else if !tt.err && err != nil {
t.Errorf("case %d: want err=nil, got err=%v", i, err)
}
if tt.ok != ok {
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
}
if tt.val != val {
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
}
}
}
func TestStringArray(t *testing.T) {
tests := []struct {
cl Claims
key string
ok bool
err bool
val []string
}{
// ok, no err, claim exists
{
cl: Claims{
"foo": []string{"bar", "faf"},
},
key: "foo",
val: []string{"bar", "faf"},
ok: true,
err: false,
},
// ok, no err, []interface{}
{
cl: Claims{
"foo": []interface{}{"bar", "faf"},
},
key: "foo",
val: []string{"bar", "faf"},
ok: true,
err: false,
},
// no claims
{
cl: Claims{},
key: "foo",
val: nil,
ok: false,
err: false,
},
// missing claim
{
cl: Claims{
"foo": "bar",
},
key: "xxx",
val: nil,
ok: false,
err: false,
},
// unparsable: type
{
cl: Claims{
"foo": struct{}{},
},
key: "foo",
val: nil,
ok: false,
err: true,
},
// unparsable: nil value
{
cl: Claims{
"foo": nil,
},
key: "foo",
val: nil,
ok: false,
err: true,
},
}
for i, tt := range tests {
val, ok, err := tt.cl.StringsClaim(tt.key)
if tt.err && err == nil {
t.Errorf("case %d: want err=non-nil, got err=nil", i)
} else if !tt.err && err != nil {
t.Errorf("case %d: want err=nil, got err=%v", i, err)
}
if tt.ok != ok {
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
}
if !reflect.DeepEqual(tt.val, val) {
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
}
}
}

2
vendor/github.com/coreos/go-oidc/jose/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package jose is DEPRECATED. Use gopkg.in/square/go-jose.v2 instead.
package jose

112
vendor/github.com/coreos/go-oidc/jose/jose.go generated vendored Normal file
View file

@ -0,0 +1,112 @@
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), "=")
}

135
vendor/github.com/coreos/go-oidc/jose/jwk.go generated vendored Normal file
View file

@ -0,0 +1,135 @@
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.URLEncoding.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.URLEncoding.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)
}

64
vendor/github.com/coreos/go-oidc/jose/jwk_test.go generated vendored Normal file
View file

@ -0,0 +1,64 @@
package jose
import (
"testing"
)
func TestDecodeBase64URLPaddingOptional(t *testing.T) {
tests := []struct {
encoded string
decoded string
err bool
}{
{
// With padding
encoded: "VGVjdG9uaWM=",
decoded: "Tectonic",
},
{
// Without padding
encoded: "VGVjdG9uaWM",
decoded: "Tectonic",
},
{
// Even More padding
encoded: "VGVjdG9uaQ==",
decoded: "Tectoni",
},
{
// And take it away!
encoded: "VGVjdG9uaQ",
decoded: "Tectoni",
},
{
// Too much padding.
encoded: "VGVjdG9uaWNh=",
decoded: "",
err: true,
},
{
// Too much padding.
encoded: "VGVjdG9uaWNh=",
decoded: "",
err: true,
},
}
for i, tt := range tests {
got, err := decodeBase64URLPaddingOptional(tt.encoded)
if tt.err {
if err == nil {
t.Errorf("case %d: expected non-nil err", i)
}
continue
}
if err != nil {
t.Errorf("case %d: want nil err, got: %v", i, err)
}
if string(got) != tt.decoded {
t.Errorf("case %d: want=%q, got=%q", i, tt.decoded, got)
}
}
}

51
vendor/github.com/coreos/go-oidc/jose/jws.go generated vendored Normal file
View file

@ -0,0 +1,51 @@
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
}

74
vendor/github.com/coreos/go-oidc/jose/jws_test.go generated vendored Normal file
View file

@ -0,0 +1,74 @@
package jose
import (
"strings"
"testing"
)
type testCase struct{ t string }
var validInput []testCase
var invalidInput []testCase
func init() {
validInput = []testCase{
{
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
},
}
invalidInput = []testCase{
// empty
{
"",
},
// undecodeable
{
"aaa.bbb.ccc",
},
// missing parts
{
"aaa",
},
// missing parts
{
"aaa.bbb",
},
// too many parts
{
"aaa.bbb.ccc.ddd",
},
// invalid header
// EncodeHeader(map[string]string{"foo": "bar"})
{
"eyJmb28iOiJiYXIifQ.bbb.ccc",
},
}
}
func TestParseJWS(t *testing.T) {
for i, tt := range validInput {
jws, err := ParseJWS(tt.t)
if err != nil {
t.Errorf("test: %d. expected: valid, actual: invalid", i)
}
expectedHeader := strings.Split(tt.t, ".")[0]
if jws.RawHeader != expectedHeader {
t.Errorf("test: %d. expected: %s, actual: %s", i, expectedHeader, jws.RawHeader)
}
expectedPayload := strings.Split(tt.t, ".")[1]
if jws.RawPayload != expectedPayload {
t.Errorf("test: %d. expected: %s, actual: %s", i, expectedPayload, jws.RawPayload)
}
}
for i, tt := range invalidInput {
_, err := ParseJWS(tt.t)
if err == nil {
t.Errorf("test: %d. expected: invalid, actual: valid", i)
}
}
}

82
vendor/github.com/coreos/go-oidc/jose/jwt.go generated vendored Normal file
View file

@ -0,0 +1,82 @@
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
}

94
vendor/github.com/coreos/go-oidc/jose/jwt_test.go generated vendored Normal file
View file

@ -0,0 +1,94 @@
package jose
import (
"reflect"
"testing"
)
func TestParseJWT(t *testing.T) {
tests := []struct {
r string
h JOSEHeader
c Claims
}{
{
// Example from JWT spec:
// http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#ExampleJWT
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
JOSEHeader{
HeaderMediaType: "JWT",
HeaderKeyAlgorithm: "HS256",
},
Claims{
"iss": "joe",
// NOTE: test numbers must be floats for equality checks to work since values are converted form interface{} to float64 by default.
"exp": 1300819380.0,
"http://example.com/is_root": true,
},
},
}
for i, tt := range tests {
jwt, err := ParseJWT(tt.r)
if err != nil {
t.Errorf("raw token should parse. test: %d. expected: valid, actual: invalid. err=%v", i, err)
}
if !reflect.DeepEqual(tt.h, jwt.Header) {
t.Errorf("JOSE headers should match. test: %d. expected: %v, actual: %v", i, tt.h, jwt.Header)
}
claims, err := jwt.Claims()
if err != nil {
t.Errorf("test: %d. expected: valid claim parsing. err=%v", i, err)
}
if !reflect.DeepEqual(tt.c, claims) {
t.Errorf("claims should match. test: %d. expected: %v, actual: %v", i, tt.c, claims)
}
enc := jwt.Encode()
if enc != tt.r {
t.Errorf("encoded jwt should match raw jwt. test: %d. expected: %v, actual: %v", i, tt.r, enc)
}
}
}
func TestNewJWTHeaderType(t *testing.T) {
jwt, err := NewJWT(JOSEHeader{}, Claims{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want := "JWT"
got := jwt.Header[HeaderMediaType]
if want != got {
t.Fatalf("Header %q incorrect: want=%s got=%s", HeaderMediaType, want, got)
}
}
func TestNewJWTHeaderKeyID(t *testing.T) {
jwt, err := NewJWT(JOSEHeader{HeaderKeyID: "foo"}, Claims{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want := "foo"
got, ok := jwt.KeyID()
if !ok {
t.Fatalf("KeyID not set")
} else if want != got {
t.Fatalf("KeyID incorrect: want=%s got=%s", want, got)
}
}
func TestNewJWTHeaderKeyIDNotSet(t *testing.T) {
jwt, err := NewJWT(JOSEHeader{}, Claims{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if _, ok := jwt.KeyID(); ok {
t.Fatalf("KeyID set, but should not be")
}
}

24
vendor/github.com/coreos/go-oidc/jose/sig.go generated vendored Executable file
View file

@ -0,0 +1,24 @@
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)
}

67
vendor/github.com/coreos/go-oidc/jose/sig_rsa.go generated vendored Executable file
View file

@ -0,0 +1,67 @@
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))
}

405
vendor/github.com/coreos/go-oidc/jose_test.go generated vendored Normal file
View file

@ -0,0 +1,405 @@
// +build !golint
// This file contains statically created JWKs for tests created by gen.go
package oidc
import (
"encoding/json"
jose "gopkg.in/square/go-jose.v2"
)
func mustLoadJWK(s string) jose.JSONWebKey {
var jwk jose.JSONWebKey
if err := json.Unmarshal([]byte(s), &jwk); err != nil {
panic(err)
}
return jwk
}
var (
testKeyECDSA_256_0 = mustLoadJWK(`{
"kty": "EC",
"kid": "bd06f3e11f523e310f8c2c8b20a892727fb558e0e23602312568b20a41e11188",
"crv": "P-256",
"x": "xK5N69f0-SAgWbjw2otcQeCGs3qqYMyqOWk4Os5Z_Xc",
"y": "AXSaOPcMklJY9UKZhkGzVevqhAIUEzE3cfZ8o-ML5xE"
}`)
testKeyECDSA_256_0_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "bd06f3e11f523e310f8c2c8b20a892727fb558e0e23602312568b20a41e11188",
"crv": "P-256",
"x": "xK5N69f0-SAgWbjw2otcQeCGs3qqYMyqOWk4Os5Z_Xc",
"y": "AXSaOPcMklJY9UKZhkGzVevqhAIUEzE3cfZ8o-ML5xE",
"d": "L7jynYt-fMRPqw1e9vgXCGTg4yhGU4tlLxiFyNVimG4"
}`)
testKeyECDSA_256_1 = mustLoadJWK(`{
"kty": "EC",
"kid": "27ebd2f5bf6723e4de06caaab03703be458a37bf28a9fa748576a0826acb6ec2",
"crv": "P-256",
"x": "KZisP6wLCph4q6056jr7BH_asiX9RcLcS3HrNjdCpkw",
"y": "5DrW-kEge0sePHlKmh1d2kqd10r32JEW6eyyewy18j8"
}`)
testKeyECDSA_256_1_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "27ebd2f5bf6723e4de06caaab03703be458a37bf28a9fa748576a0826acb6ec2",
"crv": "P-256",
"x": "KZisP6wLCph4q6056jr7BH_asiX9RcLcS3HrNjdCpkw",
"y": "5DrW-kEge0sePHlKmh1d2kqd10r32JEW6eyyewy18j8",
"d": "r6CiIpv0icIq5U4LYO39nBDVhCHCLObDFYC5IG9Y8Hk"
}`)
testKeyECDSA_256_2 = mustLoadJWK(`{
"kty": "EC",
"kid": "2c7d179db6006c90ece4d91f554791ff35156693c693102c00f66f473d15df17",
"crv": "P-256",
"x": "oDwcKp7SqgeRvycK5GgYjrlW4fbHn2Ybfd5iG7kDiPc",
"y": "qazib9UwdUdbHSFzdy_HN10xZEItLvufPw0v7nIJOWA"
}`)
testKeyECDSA_256_2_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "2c7d179db6006c90ece4d91f554791ff35156693c693102c00f66f473d15df17",
"crv": "P-256",
"x": "oDwcKp7SqgeRvycK5GgYjrlW4fbHn2Ybfd5iG7kDiPc",
"y": "qazib9UwdUdbHSFzdy_HN10xZEItLvufPw0v7nIJOWA",
"d": "p79U6biKrOyrKzg-i3C7FVJiqzlqBhYQqmyOiZ9bhVM"
}`)
testKeyECDSA_256_3 = mustLoadJWK(`{
"kty": "EC",
"kid": "aba0a256999af470c8b2449103ae75b257d907da4d1cbfc73c1d45ab1098f544",
"crv": "P-256",
"x": "CKRYWVt1R7FzuJ43vEprfIzgB-KgIhRDhxLmd5ixiXY",
"y": "QCxTVmK31ee710OYkNqdqEgHH3rqRQNQj3Wyq0xtYq0"
}`)
testKeyECDSA_256_3_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "aba0a256999af470c8b2449103ae75b257d907da4d1cbfc73c1d45ab1098f544",
"crv": "P-256",
"x": "CKRYWVt1R7FzuJ43vEprfIzgB-KgIhRDhxLmd5ixiXY",
"y": "QCxTVmK31ee710OYkNqdqEgHH3rqRQNQj3Wyq0xtYq0",
"d": "c_WflwwUxT_B5izk4iET49qoob0RH5hccnEScfBS9Qk"
}`)
testKeyECDSA_384_0 = mustLoadJWK(`{
"kty": "EC",
"kid": "bc271d0a68251171e0c5edfd384b32d154e1717af27c5089bda8fb598a474b7b",
"crv": "P-384",
"x": "FZEhxw06oB86xCAZCtKTfX3ze9tgxkdN199g-cWrpQTWF-2m6Blg1MN5D60Z9KoX",
"y": "EyWjZ46gLlqfoU08iv0zcDWut1nbfoUoTd-7La2VY4PqQeDSFrxPCOyplxFMGJIW"
}`)
testKeyECDSA_384_0_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "bc271d0a68251171e0c5edfd384b32d154e1717af27c5089bda8fb598a474b7b",
"crv": "P-384",
"x": "FZEhxw06oB86xCAZCtKTfX3ze9tgxkdN199g-cWrpQTWF-2m6Blg1MN5D60Z9KoX",
"y": "EyWjZ46gLlqfoU08iv0zcDWut1nbfoUoTd-7La2VY4PqQeDSFrxPCOyplxFMGJIW",
"d": "t6oJHJBH2rvH3uyQkK_JwoUEwE1QHjwTnGLSDMZbNEDrEaR8BBiAo3s0p8rzGz5-"
}`)
testKeyECDSA_384_1 = mustLoadJWK(`{
"kty": "EC",
"kid": "58ef375d6e63cedc637fed59f4012a779cd749a83373237cf3972bba74ebd738",
"crv": "P-384",
"x": "Gj3zkiRR4RCJb0Tke3lD2spG2jzuXgX50fEwDZTRjlQIzz3Rc96Fw32FCSDectxQ",
"y": "6alqN7ilqJAmqCU1BFrJJeivJiM0s1-RqBewRoNkTQinOLLbaiZlBAQxS4iq2dRv"
}`)
testKeyECDSA_384_1_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "58ef375d6e63cedc637fed59f4012a779cd749a83373237cf3972bba74ebd738",
"crv": "P-384",
"x": "Gj3zkiRR4RCJb0Tke3lD2spG2jzuXgX50fEwDZTRjlQIzz3Rc96Fw32FCSDectxQ",
"y": "6alqN7ilqJAmqCU1BFrJJeivJiM0s1-RqBewRoNkTQinOLLbaiZlBAQxS4iq2dRv",
"d": "-4eaoElyb_YANRownMtId_-glX2o45oc4_L_vgo3YOW5hxCq3KIBIdrhvBx1nw8B"
}`)
testKeyECDSA_384_2 = mustLoadJWK(`{
"kty": "EC",
"kid": "a8bab6f438b6cd2080711404549c978d1c00dd77a3c532bba12c964c4883b2ec",
"crv": "P-384",
"x": "CBKwYgZFLdTBBFnWD20q2YNUnRnOsDgTxG3y-dzCUrb65kOKm0ZFaZQPe5ZPjvDS",
"y": "WlubxLv2qH-Aw-LsESKXAwm4HF3l4H1rVn3DZRpqcac6p-QSrXmfKCtxJVHDaTNi"
}`)
testKeyECDSA_384_2_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "a8bab6f438b6cd2080711404549c978d1c00dd77a3c532bba12c964c4883b2ec",
"crv": "P-384",
"x": "CBKwYgZFLdTBBFnWD20q2YNUnRnOsDgTxG3y-dzCUrb65kOKm0ZFaZQPe5ZPjvDS",
"y": "WlubxLv2qH-Aw-LsESKXAwm4HF3l4H1rVn3DZRpqcac6p-QSrXmfKCtxJVHDaTNi",
"d": "XfoOcxV3yfoAcRquJ9eaBvcY-H71B0XzXwx2eidolHDKLK7GHygEt9ToYSY_DvxO"
}`)
testKeyECDSA_384_3 = mustLoadJWK(`{
"kty": "EC",
"kid": "32f0cfc686455840530d727fb029002b9757ffff15272f98f53be9f77560718a",
"crv": "P-384",
"x": "xxED1z8EUCOUQ_jwS9nuVUDVzxs-U1rl19y8jMWrv4TdPeGHTRTgNUE57-YAL3ly",
"y": "OsCN6-HmM-LP1itE5eW15WsSQoe3dZBX7AoHyKJUKtjzWKCJNUcyu3Np07xta1Cr"
}`)
testKeyECDSA_384_3_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "32f0cfc686455840530d727fb029002b9757ffff15272f98f53be9f77560718a",
"crv": "P-384",
"x": "xxED1z8EUCOUQ_jwS9nuVUDVzxs-U1rl19y8jMWrv4TdPeGHTRTgNUE57-YAL3ly",
"y": "OsCN6-HmM-LP1itE5eW15WsSQoe3dZBX7AoHyKJUKtjzWKCJNUcyu3Np07xta1Cr",
"d": "gfQA6wn4brWVT3OkeGiaCXGsQyTybZB9SdqTULsiSg8n6FS2T8hvK0doPwMTT5Gw"
}`)
testKeyECDSA_521_0 = mustLoadJWK(`{
"kty": "EC",
"kid": "f7b325c848a6c9fc5b72f131f179d2f37296837262797983c632597a4927726e",
"crv": "P-521",
"x": "AUaLqCAMEWPqiYgd-D_6F5kxpOgqnbQnjIHZ-NhjRnKKYuij9Iz7bq9pZU4F79wsODFpWxMFfISrveUfgEGt4Hy2",
"y": "AeAKfmFsfcFttwQqv2B-fUfgLhw837YoGoNWh5qNE_LqTBxbYKRUkbSLxVRHEcVNnU1t3z9yMMdYXtuMlfJ0bhjr"
}`)
testKeyECDSA_521_0_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "f7b325c848a6c9fc5b72f131f179d2f37296837262797983c632597a4927726e",
"crv": "P-521",
"x": "AUaLqCAMEWPqiYgd-D_6F5kxpOgqnbQnjIHZ-NhjRnKKYuij9Iz7bq9pZU4F79wsODFpWxMFfISrveUfgEGt4Hy2",
"y": "AeAKfmFsfcFttwQqv2B-fUfgLhw837YoGoNWh5qNE_LqTBxbYKRUkbSLxVRHEcVNnU1t3z9yMMdYXtuMlfJ0bhjr",
"d": "AbD3guIvlVd8CZD0xUNuebgnQkE24XJnxCQ69P5VL3etEMmdr4HPJLPMQfMs10Gz8RTmrGKo-bdsU-cjS3d7dKIC"
}`)
testKeyECDSA_521_1 = mustLoadJWK(`{
"kty": "EC",
"kid": "4ea260ba2c9ed5fe9d6adeb998bb68b3edbab839d8bfd2be09fa628680793b90",
"crv": "P-521",
"x": "AE8-53o64GiywneanVZAHb96NcPbq0Ml6zynIcgLdKUiHGVs2te7SABq_9keFZJC6wooACeNWWT7VDK3kY77fjdh",
"y": "AAnrIu0-D7JEd3-mqTR8Rdz53kw7DLAIypv0-_u4rqn0glwTZCkMpQ17wFH71bMInXaTi2Z_uq67NuVxFvUCaTvS"
}`)
testKeyECDSA_521_1_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "4ea260ba2c9ed5fe9d6adeb998bb68b3edbab839d8bfd2be09fa628680793b90",
"crv": "P-521",
"x": "AE8-53o64GiywneanVZAHb96NcPbq0Ml6zynIcgLdKUiHGVs2te7SABq_9keFZJC6wooACeNWWT7VDK3kY77fjdh",
"y": "AAnrIu0-D7JEd3-mqTR8Rdz53kw7DLAIypv0-_u4rqn0glwTZCkMpQ17wFH71bMInXaTi2Z_uq67NuVxFvUCaTvS",
"d": "AVVL9TIWcJCYH5r3QUhHXtsbkVXhLQSpp4sL1ta_H2_3bpRb9ZdVv10YOA-xN7Yz2wa-FMhIhj1ULe9z18ZM8dDF"
}`)
testKeyECDSA_521_2 = mustLoadJWK(`{
"kty": "EC",
"kid": "d69caddc821807ea63a46519b538a7d2f3135dbdda1ba628781f8567a47893d8",
"crv": "P-521",
"x": "AO618rH8GP78-mi9Z5FaPGqpyc_OVMK-BajZK-pwL89ZQdOvFa0fY0ENpB_KaRf5ELw9IP17lQh3T-O9O7jePDFj",
"y": "AT1x9pCMbY-BXqAJPQDQxp6j8Gca7IpdxL8OS4td_XPiwXtX4JKcj_VKxtOw6k64yr_VuFYCs6wImOc72jNRBjA7"
}`)
testKeyECDSA_521_2_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "d69caddc821807ea63a46519b538a7d2f3135dbdda1ba628781f8567a47893d8",
"crv": "P-521",
"x": "AO618rH8GP78-mi9Z5FaPGqpyc_OVMK-BajZK-pwL89ZQdOvFa0fY0ENpB_KaRf5ELw9IP17lQh3T-O9O7jePDFj",
"y": "AT1x9pCMbY-BXqAJPQDQxp6j8Gca7IpdxL8OS4td_XPiwXtX4JKcj_VKxtOw6k64yr_VuFYCs6wImOc72jNRBjA7",
"d": "AYmtaW9ojb0Gb8zSqrlRnEKzRVIBIM5dsb1qWkd3mfr4Wl5tbPuiEctGLN9s6LDtY0JOL3nukOVoDbrmS4qCW64"
}`)
testKeyECDSA_521_3 = mustLoadJWK(`{
"kty": "EC",
"kid": "7193cba4fd9c72153133ce5cffc423857517ab8eb176a827e1a002eb5621edca",
"crv": "P-521",
"x": "AQfpLehZCER3ZTw1V1pN0RsX8-4WEW4IDxFDSGwrULQ79YHiLNmubrOSlSxiOSv2S-tHq-hxgma1PZlQRghJfemx",
"y": "AAF41XT7jptLsy8FAaVRnex2WcSfdebcjjXMGO4rn6IlD3u9qnvrpR8MBp1gz5G1C5S7_NVgIeLSIYbdfd_wjVkd"
}`)
testKeyECDSA_521_3_Priv = mustLoadJWK(`{
"kty": "EC",
"kid": "7193cba4fd9c72153133ce5cffc423857517ab8eb176a827e1a002eb5621edca",
"crv": "P-521",
"x": "AQfpLehZCER3ZTw1V1pN0RsX8-4WEW4IDxFDSGwrULQ79YHiLNmubrOSlSxiOSv2S-tHq-hxgma1PZlQRghJfemx",
"y": "AAF41XT7jptLsy8FAaVRnex2WcSfdebcjjXMGO4rn6IlD3u9qnvrpR8MBp1gz5G1C5S7_NVgIeLSIYbdfd_wjVkd",
"d": "hzh0loqrhYFUik86SYBv3CC_GKNYankLMK95-Cfr1gLBD1l0M6W-7gn3XlyaQEInz0TBIbaQ1fL78HHjbVsNLrY"
}`)
testKeyRSA_1024_0 = mustLoadJWK(`{
"kty": "RSA",
"kid": "979d62e7114052027b11bcb51282d8228790134ef34e746f6372fa5aadaa9bf2",
"n": "6zxmGE5X74SUBWfDEo8Tl-YxrkVfvxljQG9vKmNPQ2RhEmJ8eplZpKn9_nlxVDHGLzfJMqqUBS19EarIfDnOqyFBRkyKRbsQdUVU9XLjDIXqImJ8aN-UmShAYoeOClJVDsBJuxBGgS3pdgG7u3YQpvTV_hGuMoJr7UYgIrqb0qc",
"e": "AQAB"
}`)
testKeyRSA_1024_0_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "979d62e7114052027b11bcb51282d8228790134ef34e746f6372fa5aadaa9bf2",
"n": "6zxmGE5X74SUBWfDEo8Tl-YxrkVfvxljQG9vKmNPQ2RhEmJ8eplZpKn9_nlxVDHGLzfJMqqUBS19EarIfDnOqyFBRkyKRbsQdUVU9XLjDIXqImJ8aN-UmShAYoeOClJVDsBJuxBGgS3pdgG7u3YQpvTV_hGuMoJr7UYgIrqb0qc",
"e": "AQAB",
"d": "IARggP5oyZjp7LJqwqPmrs4OBQI8Pe5eq-5-2u4ZY7rN24q8FpO4t8jLYU92NVdw-gxFvjepXesLEtSD5SSZFD7s-5rqmX9tW_t5t1a1sBY6IKz_YBSf2p2LtdTyrqgo4nRD0nYH3sMvGtOKCTV4K_vwfWPD3elXq752trG-HwE",
"p": "9tgo12L3earNNrJfyIIDD2dqgKuNfWhQzhbe-Ju17dF37uGF6xnLqR0WXU-agGpUdYTMOd7IQi_bX_xkjQBYlw",
"q": "8_YDvufPdccjGM2rAXs-2LbeP_LrzLUk1uGNpEjIt2lXEm6sQKjIfNLcAfVKh9zsqwqroveL2iI1ijsDgNAIcQ"
}`)
testKeyRSA_1024_1 = mustLoadJWK(`{
"kty": "RSA",
"kid": "4a3a37dc10aec5f427509e4014683102a0fafa32bec2ff9921ff19e1a55c79e8",
"n": "sn7sU7dBEkU1RBIz_LKgrswSS7-68vTlOe7n-lanAqAlczm01_6IWrvcIC7lPv1iHQqWngusskANZirCZGTv6kK8kJLHBVRSROB9VkPTPNFYnSB6dacyPa0ty0otsaYOVM2RFvcCX7lBKKat2Tmst8vITdpvnoEzTgT_eGOKGAs",
"e": "AQAB"
}`)
testKeyRSA_1024_1_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "4a3a37dc10aec5f427509e4014683102a0fafa32bec2ff9921ff19e1a55c79e8",
"n": "sn7sU7dBEkU1RBIz_LKgrswSS7-68vTlOe7n-lanAqAlczm01_6IWrvcIC7lPv1iHQqWngusskANZirCZGTv6kK8kJLHBVRSROB9VkPTPNFYnSB6dacyPa0ty0otsaYOVM2RFvcCX7lBKKat2Tmst8vITdpvnoEzTgT_eGOKGAs",
"e": "AQAB",
"d": "KTXqpE1sBaba7HNzc0Vemdzd4IVMyWlHPz_saTz2ZEHLQ7YwDapjmudCpF-PaCKiM2hNbAHwBluJfGwk4372cPHcJyri3Gs4GGj-cOlbXJh9aUp5o8fqn904B1UcPxMZ2DrZAKjseuLj3zyJgCsSJOGU1kJFRAkM8UN4a9cE8sE",
"p": "3ntHMGDJEXwqldLNR-DucKGp32aJVAaKnn4j9qy1Tj6KhH70o3HyFVO_TKYxGg-pG3zTV-VzyMqmeh9hDdnyKw",
"q": "zWMxEjyxvp-P-mNxhWQe1pJXZi6iPpcnt0SZLfweT3AtAzEaoSs4-4D1jT911LD4xDI8_a7-gYHgD8ME62PhoQ"
}`)
testKeyRSA_1024_2 = mustLoadJWK(`{
"kty": "RSA",
"kid": "d1c32aaea6c9527038e589dd94a546ca1beedb90f90e073228986cc258cf1fda",
"n": "3mZrDaB5-7e29zok9XkGuu6FXYB00FqFgnGTJAGCMpql5uHz1h9p0DljZL4vsGkkYOZUMvqFS1pCEuzdSsupPNClf0NKMRux6yLv6iIR9C4pE9RBKPUrinzJuYs634rq5JOEP4IpP_fJfKxMw4Na85otd9KposKwP14cCkOibYM",
"e": "AQAB"
}`)
testKeyRSA_1024_2_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "d1c32aaea6c9527038e589dd94a546ca1beedb90f90e073228986cc258cf1fda",
"n": "3mZrDaB5-7e29zok9XkGuu6FXYB00FqFgnGTJAGCMpql5uHz1h9p0DljZL4vsGkkYOZUMvqFS1pCEuzdSsupPNClf0NKMRux6yLv6iIR9C4pE9RBKPUrinzJuYs634rq5JOEP4IpP_fJfKxMw4Na85otd9KposKwP14cCkOibYM",
"e": "AQAB",
"d": "Ft2M0B_ZqsmepBh0SFCjIoD3cT-NwwYrh9fJewA0tKM1v2EnwrIEHQZpc6giGw8UUGod6gfbwH2NIYj8z33U7lyrKWPR7F8gJRm1KR5NLCBCnAnz0ukhsg24ktB25LZLZRCStRRhtCev95Vvmew0ip5081hv730Z2T_PsEyOU6E",
"p": "8moFzLKJSQuhaIYTo1qNX0w8o4NBFb1atOlthHmq6Y6rdYTm_nyM9Q3mrNBolPS7LHTiBXtCEJ_m4V5hWDuGKw",
"q": "6t0_mZKOoZ5NAE--MxkuOCcRhuqNo9VoacfPEH39CeoKAam4v5k56R7aQcb_raQK396pgV-evM2dpfdUaasiCQ"
}`)
testKeyRSA_1024_3 = mustLoadJWK(`{
"kty": "RSA",
"kid": "838742d0e3195b19f974fa30352db79070fa19aaaeb678ec95c499de7bd48d52",
"n": "17Uc89-QvCjqBLXDJSCWxUoohjwFPI63Gub8g-lH1GSK3flXqiohz33KfKhqrdKsLrpRjskGTMg3Vo0IcLBnMYdp1i1nceORghtYLQsDNS7tqlHiKx725fLmWldGgiuP_0Ak0Knisw-j_q7Jx3OVAJnuS1o3vPLfJxJdyq6yV1k",
"e": "AQAB"
}`)
testKeyRSA_1024_3_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "838742d0e3195b19f974fa30352db79070fa19aaaeb678ec95c499de7bd48d52",
"n": "17Uc89-QvCjqBLXDJSCWxUoohjwFPI63Gub8g-lH1GSK3flXqiohz33KfKhqrdKsLrpRjskGTMg3Vo0IcLBnMYdp1i1nceORghtYLQsDNS7tqlHiKx725fLmWldGgiuP_0Ak0Knisw-j_q7Jx3OVAJnuS1o3vPLfJxJdyq6yV1k",
"e": "AQAB",
"d": "B8cM-zIVauNixLa1CZKqPQTWfziMy8ktivfHJQ51O5BAfY5u_cC1JWEYuvPrnMba1Hh9VlOjOYOCk0lUg5OotMx5hxym6M5y_2rIrW90a8r3gGttPlZmyHQnFIgj2QZHlEyZGU1SIPTOtoECW5RGk7cYpA1s1_zfz8uyG-MiCPE",
"p": "2dwia5iWWLZwFcCVylPh_7QDr3Wxt3kW_TmHIbaRT10Rborj1lAwltyEx7aVpHq-aNfvrYIEBbOXWwUU8PDUrQ",
"q": "_XiDT55hn-Txi2C_peMVSDgXozf01qHKvdLirXM2uT_CI8kruYZYjWYn6_cSUptJ60eioM3WNTxjrSxWRS923Q"
}`)
testKeyRSA_2048_0 = mustLoadJWK(`{
"kty": "RSA",
"kid": "13b668c7b4f5d46d53de65e4b9f9c522fad4870e4d1656e459c88de6e90984d6",
"n": "1j-CxBxkn20C_M24t6ueLp02T4MMAiPLXf5yaDcj7EcsXGbnsGEhrAUwCZwdEJAxeZ0PolmlE0XBOhQfRtsCVJmwu918aknptToyDbOUBr6WtPIK_c_BuGVanLCx3SnczV4jle9Bz7tGfpj2vAXytSBrfnZhdCHNEFeefQTQMnavfMhfWf_njiTa76BRyAHjb-XZIJHKovwBu0y3glmzhSKYNsUrW11RsWx6DbueWEbE3FpsHTiEdnJipcP3UKl3Z2z6t6n9ZYtFWkx4zCVVBQu-RWUQwjr2XnR1LFwXgL9xQocDBmS1O-wMTHqL3_oosNUdV3vuMPdxs_SEoys2rQ",
"e": "AQAB"
}`)
testKeyRSA_2048_0_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "13b668c7b4f5d46d53de65e4b9f9c522fad4870e4d1656e459c88de6e90984d6",
"n": "1j-CxBxkn20C_M24t6ueLp02T4MMAiPLXf5yaDcj7EcsXGbnsGEhrAUwCZwdEJAxeZ0PolmlE0XBOhQfRtsCVJmwu918aknptToyDbOUBr6WtPIK_c_BuGVanLCx3SnczV4jle9Bz7tGfpj2vAXytSBrfnZhdCHNEFeefQTQMnavfMhfWf_njiTa76BRyAHjb-XZIJHKovwBu0y3glmzhSKYNsUrW11RsWx6DbueWEbE3FpsHTiEdnJipcP3UKl3Z2z6t6n9ZYtFWkx4zCVVBQu-RWUQwjr2XnR1LFwXgL9xQocDBmS1O-wMTHqL3_oosNUdV3vuMPdxs_SEoys2rQ",
"e": "AQAB",
"d": "C8F4X2Jfcw_8Nfrjw9A64bvmmv5JzmRAaGvpwyYjZneRS5Cp7demjVXLiPtz7NC8pjuj-_iHQkN1ksY_4RdrTVERjX1dskdT94m17WKJIMWcZ1lQmRSpQIDvM-HOIKCHaQ1dToDOT6Oq_o9OGosJAj9BJrNALasdIWRtYda9xcb7roLl_U3AtOlK9RiFygtt5uVBIPh1rsxaT0Y1MvMk4EMbFnv7NXXk65UM_3p2leSoPpO7LvlYs3WoRRX9ABH7zI-ppwLGEDuxfnwKzAOaRBe9oUh5rTYdazaY9XqofvekBc9Xqa2HpjkYX-L4Zy3oj04u-u5zX8KTh25jFC2a0Q",
"p": "6L6icXqV7jLSXHrhMyLpW-tdFFp2XRQ_uKk972jmUJ-sEeh1YYSx_JpK6oaUnYshGbRLfEAOLy03iweAL4uMZJcASOR44TwSnn_qJZ72PkwhD42uKhE0eJRpxVht6jf5qhsynaMNUWc_FIW5ESpQIf6j4rqFV5WrKHAt0ypChTc",
"q": "66fAza4rLvyjqQAIGQ07RiRYW4no1h4cK8RH_CDUgI3CCFQSuqTB8ZqM38r2u0tZoKzCNgHcnde6Jk07X6-LSQW2kypMXt3idbgaeVSYSbdZndgG_jTibJvoybxSjo6HfLpPvCrfsihXo7O5M13LE7VqBLbGTLI5iubOxcrfFTs"
}`)
testKeyRSA_2048_1 = mustLoadJWK(`{
"kty": "RSA",
"kid": "6d2b0efff0011bbbbf694de684b5b97cf454fd4086fa53b9af92545f3fa65459",
"n": "1CmSBUbxU1jjnmaBG0r0cLLmyZgCOMbfpG6Z3HwfVDgCK6P-rU3F6QQazrOgGJJ0sz6vP50VK5u7BR6vSrPBBX5CicJPM2iNdz2JuV9ODEIkDQBLeI6TfIGhNOls-14tXKOPExY8b6JdyDMP31Hwo_0pF1FaunZ7yY1bgoKCtDV5-RKGd2EgylDGNPu0Ilr92MqCsAntBC8eQSkO4CcTcti4t9cX45VY5nPtwQRmp5zIgUHnU4LV3QVTLJnU-uidaAxRVQbS1pVql5xR6nYZHYvFk1IU-wTY6gGk5WvGWWQ440UTTaMAfnJP6VFDggUXeGSlKfKkDcz7JLT2Ma2KXQ",
"e": "AQAB"
}`)
testKeyRSA_2048_1_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "6d2b0efff0011bbbbf694de684b5b97cf454fd4086fa53b9af92545f3fa65459",
"n": "1CmSBUbxU1jjnmaBG0r0cLLmyZgCOMbfpG6Z3HwfVDgCK6P-rU3F6QQazrOgGJJ0sz6vP50VK5u7BR6vSrPBBX5CicJPM2iNdz2JuV9ODEIkDQBLeI6TfIGhNOls-14tXKOPExY8b6JdyDMP31Hwo_0pF1FaunZ7yY1bgoKCtDV5-RKGd2EgylDGNPu0Ilr92MqCsAntBC8eQSkO4CcTcti4t9cX45VY5nPtwQRmp5zIgUHnU4LV3QVTLJnU-uidaAxRVQbS1pVql5xR6nYZHYvFk1IU-wTY6gGk5WvGWWQ440UTTaMAfnJP6VFDggUXeGSlKfKkDcz7JLT2Ma2KXQ",
"e": "AQAB",
"d": "DYJcHuPmh-UYEUT7oY5DRE3P7jQ0qALZyLGWMHji0c0DLl4x4D0chfrR7il33zisH6G1LPrGl1FCNlA-3yXU-5GPkRADVQWqRFZxx5Du-k7X1tAW_iUt9PaYGjNm0hasEsMDYDbBQGZ5TD8cGp8wEHEVRbvTaB4VQb8zfXrr8ad8XCFdtYQF5Sw_VuvUBcn-50kdl7S0MmQiwLx5xkisJHkVdVsrWbq-JJPMYrjYzPGo07kt8pQH2ecxrQD2waTzkLiAg-nytPBkAyMIFQ-XCulWCNiI-V_HJ0pqNctgVpdaSihlPCYhfllxUSU8yg4UvcfFEAN2S5yjvPyrJeJLAQ",
"p": "4db4J-vxAyb_3otKidzYISIo8NuPxPXuYeImbvVbY6CWHdAYAkPgbwADh592D1vM6kwqZLot4VQ4XUVtX7Vf8tCaq5BAzcVAslK0rhK86kSmzuvtpS1ruzm1DGwJcQWOl_9qYnaAov8Ny0NG8fTm8iwvWJH7rVa3XNtPL0t_fx0",
"q": "8H7_sX4kY3_4t90HbA8IvVkTf3-OGaIjtcDn2J3HLq1wMKAtdxwsbMtjhLYP70PPBe_FBNpcWhE7p-tOVhH1i0N0wwFqwlRQy-3Z4oiLeSsy0Npl29cfP7teO5UTrbrqfmHB0j2u_nEqA4n4iDF5QhPr_9yzxSR4Pg56z4PgFEE"
}`)
testKeyRSA_2048_2 = mustLoadJWK(`{
"kty": "RSA",
"kid": "7988e9dea9647281b04d094e4dc5737eaf739a412a570aa4d6deed7ee5640c30",
"n": "wWOFgHHiJ54ZjQEKxvfqYDuhYymbfvYxvreroqx7E-cAuU4QBsOvKV3HNnH_vDRQE1AlqihNWmPFLptp7wxdMrLEtDnRFqTxyTXJ7wLCaaaB6Wwx1dhpr9QEG5-8rxLGFYv2w5i0-o76JPHG0tuqf0YNmHp9oWcv524XnDBuji4-u6km1KynT1EQKHYgy57JWgUpukgJGImBEvf0hC2Y5s5mHvVby3xAD9-cFa2o9Vj3G-SBYFsrRIVR8GcVNwo88oj8F7-2nGD_wOOu_qT_5I3vfToUYGj5-2rAK1ja0bZpXFibrkPrW4w9paG-3F7I0sPeL3e_BDC_rQ8TgL03uQ",
"e": "AQAB"
}`)
testKeyRSA_2048_2_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "7988e9dea9647281b04d094e4dc5737eaf739a412a570aa4d6deed7ee5640c30",
"n": "wWOFgHHiJ54ZjQEKxvfqYDuhYymbfvYxvreroqx7E-cAuU4QBsOvKV3HNnH_vDRQE1AlqihNWmPFLptp7wxdMrLEtDnRFqTxyTXJ7wLCaaaB6Wwx1dhpr9QEG5-8rxLGFYv2w5i0-o76JPHG0tuqf0YNmHp9oWcv524XnDBuji4-u6km1KynT1EQKHYgy57JWgUpukgJGImBEvf0hC2Y5s5mHvVby3xAD9-cFa2o9Vj3G-SBYFsrRIVR8GcVNwo88oj8F7-2nGD_wOOu_qT_5I3vfToUYGj5-2rAK1ja0bZpXFibrkPrW4w9paG-3F7I0sPeL3e_BDC_rQ8TgL03uQ",
"e": "AQAB",
"d": "RPC4j-CJUbw_uY-Miv-oMuQvFU2o3Crh8u5BJn28ZozsKiMU_YRW9jUzJkqfczVm8muY8b7qTHXSvlmy-v_6XW9zRhhyXFMyypr9QNJIAifUmiTy4xwCGSdIy5w3RGY57UZ3EqVmpwe_TtpOGa8rabHMePX5wUcqwaLykcCGOPLOFKfPF93GqZYi2StS1IaiijLi2IAMhxQGqS6Ct3dA7yacQwegPiDzjb4Is3V9UQ9k_aS3I1w56tz5qspVmfDEuuWToc-2Qyk-eMKWl1tTDJuGTCiS5IFtMPerRpPq9GCpycV0csIW-6AnW79b038DxWjDAUsNnDPnX0y1dQ1G3Q",
"p": "yh2KH_JqU29L2SLG5-ul1EBbaSbt3F5UUZbCvm_MIsiH1TzafVyBd8FeG1d-pMR8bgGjaDSA-DNkOEDAeAlLx-UCTl2Uk2ySoMnZr5JjVLW2DTuF7e8ySKrKQSSLe63aQyKuIZh9P1RNrwrmFwvJgXdzudVjirmdTwxAN0lijE8",
"q": "9PJitAE_kk6SoKvJisXbDb7Xursj26xiYIdWuzlzQ1CbwYcJ1oORDpo0bEpy0BfIbZDiJJ79Xrh5Lo1BVysQ-IZJCXK7mavqiSa8g3B8rmqLXmAtjDbwLGsJOayCpYnsGgOLT7XIUYXCFH7C-tSSydhJ1j8JAbsJveMiBPKnUXc"
}`)
testKeyRSA_2048_3 = mustLoadJWK(`{
"kty": "RSA",
"kid": "d214aea9b78d15dd2667f5d19c36df6647fa50023140fd4f5866d285718b897c",
"n": "qFnqWpBOZ_5jnIr_XkXnonmHII5gKzxPhUBfvWBhGN2eH8nmnGh6aUnKyKuCLP_qfYLU7cf9bah-e00451iGite8Tg9ZMYAPFX4NM5j0rGWNv9Z6lSn1xXezJv-FU9VUwXm0DG5eVcB8OV99JWxHivQUSBzY0Q3DUlgrD7FhMd7BrrcmTUO07KnW6SxN3oTbT7fNMxfJSTRxmvU-t-6sLhYP4Wbg39zEUgI8M7b7tvHp34klSqOn2DibVBhvWGF1-IgNT7ng6pS5iAZN7Cz7NjPc9ZM_IWyngPKZV49Tf-ikX2O8uiCb4ccgRur6znwkE7MPrDTXSMHALn10sUcO_w",
"e": "AQAB"
}`)
testKeyRSA_2048_3_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "d214aea9b78d15dd2667f5d19c36df6647fa50023140fd4f5866d285718b897c",
"n": "qFnqWpBOZ_5jnIr_XkXnonmHII5gKzxPhUBfvWBhGN2eH8nmnGh6aUnKyKuCLP_qfYLU7cf9bah-e00451iGite8Tg9ZMYAPFX4NM5j0rGWNv9Z6lSn1xXezJv-FU9VUwXm0DG5eVcB8OV99JWxHivQUSBzY0Q3DUlgrD7FhMd7BrrcmTUO07KnW6SxN3oTbT7fNMxfJSTRxmvU-t-6sLhYP4Wbg39zEUgI8M7b7tvHp34klSqOn2DibVBhvWGF1-IgNT7ng6pS5iAZN7Cz7NjPc9ZM_IWyngPKZV49Tf-ikX2O8uiCb4ccgRur6znwkE7MPrDTXSMHALn10sUcO_w",
"e": "AQAB",
"d": "cCIT2uarktD6gFaE6cIeGzZfLuwmWiX9wX-zRWxgwDM9E2dj12IvxtmD3E2Ak4CSK69tLEQ9JUFJnc89y7pHQ0uW_VdzzWjCo0omeOu0bO_njpPJanlcXn7wMVWY9NHvdj8eEfmhk_R1ybE0piyNKpyQtcehEv3bz4kyhW1ck936zafn5GWxCEWKIF-7OcotxtOl6z1GrJ7WqglMk8ooLucnAXzaPtcvD6seUhVH0vG60yI2AEInI_jMHR-mfDxrebG2xPPJLsqH3GChqTsxG5vjuaq71sNBiY_TbaUDbmWZxV66zdjhN93Rw-lc3vCPwG5vmuA2igWH7SKsHmQOAQ",
"p": "xvTQi8nCJGyZQm3fr7HU23FkSUYkKFUN1FRDWqjyz7kFj5rJxq5dSsNrrgVBvIBNY4TwDDcYia4rvx7KSRRwfV_rsU2-TCOxBebqVlbeV0HI7xMcVZXsv61Eugz-dznUU4wqmP_rCQ8vyNcVvCBbBLBW92n-IRQXsfuSUfuHnT8",
"q": "2J64YbgvH62PB4h-0iYuaykgUzWVSrXENg3_GaF8877AMTe98gjcH7HzTDW1fOhIcNZtmYTwlA2SgFudLsIJLoOn0-kmHzZjZJTWcsZsfTVwKhHD8Lzic7QkrrlbTIZlVu4mfT2f0kvaQiZp6zDCke3SLvVjlD-ebqTFCDIlXkE"
}`)
testKeyRSA_4096_0 = mustLoadJWK(`{
"kty": "RSA",
"kid": "4941c1e95df518587e54ddde3510c084fedf92098473537fb3fe022eaece9b42",
"n": "ypJYaH9_PLdEgGN3WFPL1V9qhC2BRXr24f1kGocaQe7wORFlZGX6gRpOCQ1lLmziLafERoSqZuBWF4vSGLeanCRQ7UKObYalMRR_rTJk6D_VD95LNOmtR4DrucdZvjQrAuLXhM5dNLGVoSq_zQowLDMeLWDUBB_TK4WlofoxI0RVoUOt4UXnSev8M_6yIYASpOAk8gG4FAqRfmU-3JhgkZOQCD0BhKYZVw_kEhRBrS3aik-1pQkan9hYIxWwc4KKtrLHNls7--vk5xDk2Vod5sZYRLlimbqHavN2IBeAeJGJqt1grXOVlnagKqmyq2MoPKufwptJBiVrVsTplyUB9FZe9FxUfxrkkxYJHufwP-3wXhaXpPFdL86TCUuOz-jTfng1rsTpwzmwm3RqFzz01yMc1RhaeWininQqj42bhW4bgFGVRP2QqSFbCbRZ4WMW3qWr3aR2QpJ7b8ac7JUhAJoh7-UtMmyGx9UJP0gjA8Wm-O81UENoDjMMdNUaVTfpSUKB7xZppg244jNZDIm1ptq4QyvvWkr0brp9Ymxpu0e3g2HyywyPjbeyNrIelb4E2tU5vCR4Fs_zGFG71AieruJGfzPTXE7ZaiLhP8n1652NCzIH76Y7OGz4ap2D55-RKpUkSI3KqWxUKxkM0tuZ7LS2F-1wEVs8P01K6RuQpxE",
"e": "AQAB"
}`)
testKeyRSA_4096_0_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "4941c1e95df518587e54ddde3510c084fedf92098473537fb3fe022eaece9b42",
"n": "ypJYaH9_PLdEgGN3WFPL1V9qhC2BRXr24f1kGocaQe7wORFlZGX6gRpOCQ1lLmziLafERoSqZuBWF4vSGLeanCRQ7UKObYalMRR_rTJk6D_VD95LNOmtR4DrucdZvjQrAuLXhM5dNLGVoSq_zQowLDMeLWDUBB_TK4WlofoxI0RVoUOt4UXnSev8M_6yIYASpOAk8gG4FAqRfmU-3JhgkZOQCD0BhKYZVw_kEhRBrS3aik-1pQkan9hYIxWwc4KKtrLHNls7--vk5xDk2Vod5sZYRLlimbqHavN2IBeAeJGJqt1grXOVlnagKqmyq2MoPKufwptJBiVrVsTplyUB9FZe9FxUfxrkkxYJHufwP-3wXhaXpPFdL86TCUuOz-jTfng1rsTpwzmwm3RqFzz01yMc1RhaeWininQqj42bhW4bgFGVRP2QqSFbCbRZ4WMW3qWr3aR2QpJ7b8ac7JUhAJoh7-UtMmyGx9UJP0gjA8Wm-O81UENoDjMMdNUaVTfpSUKB7xZppg244jNZDIm1ptq4QyvvWkr0brp9Ymxpu0e3g2HyywyPjbeyNrIelb4E2tU5vCR4Fs_zGFG71AieruJGfzPTXE7ZaiLhP8n1652NCzIH76Y7OGz4ap2D55-RKpUkSI3KqWxUKxkM0tuZ7LS2F-1wEVs8P01K6RuQpxE",
"e": "AQAB",
"d": "VQagPRxm16FFC260hUqG4ASwvNIs1HEMd0bYYZobl1knU4zNthpnzxCveHU65wWk2ez1IXRF4fB_slpp0R4fszI7FZs-FRLS-4rTHGtul11TnNl9T7RVmxGt38ihDojvFMMKGyBTVu7DE2bSIsoH9kVugTWHSEPjav0pzJcrUNY56vpxXYDt18VJkrlxI0aSjMnYOAwoq6DT-O2eORFsVy5M4mhY3sipEjYFUOFXv8zjUfKrF55-omE4fWF5MsK0XoMjwtkAkHkvFx2sMN72dgsCubXmgQgeFvIhvs6eifzsf99z2NoPC5y3FbEs4Ws5VF3lLNXpDL9gEoeMVHigHKNoztIzbJnFLjzEag4wOBqbig4nWIlYEcLwd5-E0oQ8GHAhWtJn5AJQKOwTcasRWiUnRAulCcI8f9R2ATTSjAGU-juFUfhProp4w3ZlnUbO6U3tbh90KdFSJa-Bapts6ijPgEp6MHubVCHIxh1KGmod_CuGGjze1GuISEwI_4Yh0SrFlp4Wy6ecg7mKNnXvkBMd6h90LJaTzubnts33cCs-5UzCtYqmWHwXrMA6dRJuicY6su3NXxStuZn2bxU_Dn8LrS1vx_lhDU__NqMNbYj5ZvHkPvZiJNBI6Z1R5SY3pQzpH6vh9W2KRKF8cKOkushIaHNnHo7W5o4eZ9HjJFE",
"p": "7Bxd2wTrXfeSZAEGZEnfoJaMh6hKFG-fx0wLmF9U-Myq1fCIQ-gdl_Atqy0xQxYtQl2lOPUWfySegJSxpdRF8pwvLq9Yt7xFQDrV0B8d5-FjYJ_Mk2bbyHv_PhNmnfcYHqE_VdADeSaU-MRV1iknvjdbxSxYUF-U2C6llfJhn-ZbGPmZJyz8fTWHSh8aKLPJkuYV84wcNL04hpFPJq7Bhy7HAFhRYNpxXyDO3I36DCU94uvUCY758VKLl8IYMMSqjh-thluKJSkWibo85ZOJR38XFksMr70pH-DkT0WMv3PpLtNu6ac6Ry4CEkZexSWmbVk-pduRooo0On1CMsrsLw",
"q": "26K4gFJ0Ygqr4KaJ4QcRGN29XcjDpX5XTlHy3hCs_wEWc5wedydVgIH4tadiZcu0WinTtl0I6FYewWNpI8nFh8XJXsLOLepdKKR798zW8kEhzxdYdP1MTtGfy12KUCMeS2RQRsBJlmNdgrI0_m0JvW2LlGeK0EqvrqeS06L8bKGXkfY2aFGHl6ipSfUd5OpuSUqgfrj4-HgB8zywm-yAvn7UK-ySyQrP3K4PuOiY5QZmcPDxl8yqDAjorStrjPieYrlMqREHMsyq2FtAXhx4FzpKN7OljbE3EpgyFKkFN-wDTWRGxu3au0hQJx0-XthZ9i5r8zGcEbArQ5jZH7CQvw"
}`)
testKeyRSA_4096_1 = mustLoadJWK(`{
"kty": "RSA",
"kid": "d01929d205e83facdc5080a0a4ade80853978298a5c73780e863c035d32127c9",
"n": "ve_rSTi1vwfV2Oj7YNu1kv8Xz2ImAEqf7qo2RehTYW4FJpbj0wkMBVGmAulm4dz-1knnaPdpbrrIwkEYr1XR6kSIeB8aXkCZqDGZATxLSRqGEzu2J8Xt9z_qTQLWYD-NuMEf9H3B1CiP3Q2RnoWUlGvOr_KI5h6anzeqgZxlQLgqQujXscwqWpX7MVh3sheZ5SbyTkDcFWvQS_NEPaBGso-Au9X-yhZsHU1Ky02Nd_DPOrrRzl7uymE07hy3bIbxmrh4ZeEz6P2ixsHHYbd15GNMRlr1cWbg91RBB-akSxX0VYoLjjuqwo33UHk1hBbSATbobfpOKRruuZPZ_xOiPi-m0tUdD1Pj-h6xRcA5sZ1d55IdL8IY_9kgLc_WyU2RWFTXV6zA2SDDdE6EdEB_8q6U0oLU5T-YRxd-V2qOTqW1388qUaL5BalSvqM0g8kVfBYIM3uwLqQI_hCer4CL7_0DGlUtheye3qcoyqWVJd9iqfcWC-1S8NljAJwM9sehx6kOSM85UuUTH89VM35oAVp0rSPcLy20dPzEVQ0LAcy_iRHTkqy9nZ1z6mo-IWQE2ZyPCuESEfG7nFJ3YlUfwBJ5uZnk0N1FVYX5zgEKdkP322vShrv1blB_JtUedpbCcuS-qCaywpwjL0pzTDKpJMEqHds0-DFT7AbULcujw_U",
"e": "AQAB"
}`)
testKeyRSA_4096_1_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "d01929d205e83facdc5080a0a4ade80853978298a5c73780e863c035d32127c9",
"n": "ve_rSTi1vwfV2Oj7YNu1kv8Xz2ImAEqf7qo2RehTYW4FJpbj0wkMBVGmAulm4dz-1knnaPdpbrrIwkEYr1XR6kSIeB8aXkCZqDGZATxLSRqGEzu2J8Xt9z_qTQLWYD-NuMEf9H3B1CiP3Q2RnoWUlGvOr_KI5h6anzeqgZxlQLgqQujXscwqWpX7MVh3sheZ5SbyTkDcFWvQS_NEPaBGso-Au9X-yhZsHU1Ky02Nd_DPOrrRzl7uymE07hy3bIbxmrh4ZeEz6P2ixsHHYbd15GNMRlr1cWbg91RBB-akSxX0VYoLjjuqwo33UHk1hBbSATbobfpOKRruuZPZ_xOiPi-m0tUdD1Pj-h6xRcA5sZ1d55IdL8IY_9kgLc_WyU2RWFTXV6zA2SDDdE6EdEB_8q6U0oLU5T-YRxd-V2qOTqW1388qUaL5BalSvqM0g8kVfBYIM3uwLqQI_hCer4CL7_0DGlUtheye3qcoyqWVJd9iqfcWC-1S8NljAJwM9sehx6kOSM85UuUTH89VM35oAVp0rSPcLy20dPzEVQ0LAcy_iRHTkqy9nZ1z6mo-IWQE2ZyPCuESEfG7nFJ3YlUfwBJ5uZnk0N1FVYX5zgEKdkP322vShrv1blB_JtUedpbCcuS-qCaywpwjL0pzTDKpJMEqHds0-DFT7AbULcujw_U",
"e": "AQAB",
"d": "mCjZ3wDVaMJIKMsMhx28KpS9aGACfX1K_pHRhNOH6KeQ7Mc4oFnBDYnJas-8ofi_FsCB6G88QX7VUfmAYwZncjuQ8FpKb3NlJX8GSh0ZWukqu8G8PcSszMShWSyKvPRs_rOIe_87BlGwXrB-FfaBfx2WqRGtZlziFecsa0T1QJHJGW0bTs52p7c7Ut7ClSOfIBrBRrtjFK4YYp_x7US3HlkkElZvFUo9NoQzBQeN66Y4_Z2ocqFOv0Z8drz-nKzGZOKfYU62nVKD0qJurfOhOGPsOPipZD28v6b5qfC1cYmXAefjNgDK3a2JkShpHPaDKoHoViKN9xQiZvzxSQ1bjQCgB6xQWk61YwmiB44gFLshXvXkx_wcccO2XC4m-Pk0r-Uj9Ugvgizk8HYp1H9eqnp8A6sqU44mrmdt14vM68SGzEqYpLoUkcxPttHvNoORAzgQfuK3SOIFyuwFqKTMBupR4A42XvxLj_PuKEKFekS2JEK8fON2t_LuUDVemjMxwXD0gjljHSKD9HnguihI5IEd6Kgr-HlTwiiDQfzcmWQdoFUs_Je4tVCSbGRrJYju01gLouDu7gzN14pZcEyoBLPkj-_MDiiymvlBLOBU2s5nQRZCaC_0IKgHLaT_rzQaXwmVhU-OURdwdoViMVc_cdXtle4TqU1g41nNE_0tiwE",
"p": "2h_9HZCOpf3S9qpAkNS2ncMsRjBlWCpRltoCs-XBF_IgOJlCkJ-42k_V1zaf6YrgMy00XD4Ij5MMiUOharvc3w_WKJF-AwGu9JEpi38Wzj1nkY86sdB1ZPu4ihSRZm81jIjuLZ2siILqkupXaY0S_0CeNLKPoVpvfPWCb3psCWuLVRDhYECPCx9FYg2tzisbFpVAPPqNCFswMYNvhyJ2NQ0hzueKDc1uHBUanCSG6gIbeY1bQvYYFGySYkWZcQg98RSeDbLysHalCSQCgPjss3likTT9OjWLv23vpXURFhgywv6Phx4He0yWB8ZRrnskfW0S76kZ325SPUlnOhf31Q",
"q": "3urwELOa7TDpwLMeMEOf2aspz1gkMjoN-V4SMzRGxIoPy93-bQ62Z8nPduma1gK0zHOIs0gU8dDqjy8P8Z7vudJqRFJIrzjD-RnDqQDz7KM_6x0kbEN4GLQiNGVAckRhoEmYjDXXkG3teD2ST5NqlYwxGvFmCvw5RCJZG7eNjXY3KnJQrDyx61NIFEvSm3sBlJkOVhSanuqHBL6T-efaBkUcQ7mY7_iYtCt7cjMG8qQtF0KovVsMF6BUc9KDobpUvDsvPrSTAzsYJvVNkzGJtF8Strr-61VA6qZnRcDW3ukYvmJp_SdAcv3KzSkWwEmJEaAtaiOh2jWkRG9QpV_LoQ"
}`)
testKeyRSA_4096_2 = mustLoadJWK(`{
"kty": "RSA",
"kid": "822512e8098b9fa56c9152a56d2607bda912c89f6f4fe1b6ccf41627bf06de29",
"n": "3bcg0diA0TPvK283P02o5UT8cBf5q48uUsfuCkIbLqNFtskGuoft8OJLmik7c7pLNSK3Jad_-xRlfsrcV1fDBs8EJDFnoY-4r_xk_-GQl-OsHzuz7HMvjVYUSYeVZMtzPAfCIFJxSIaiIW9MEwCwYyrtRefedfKUV7Ax6hjOUXtyRZ5DRHj2FnNl6MjE8mTSJ8OQI9ETErg0wF448fJb6hYCidVHfeBSCOceljXMg8a9iDYHVnzsUb9ETl7xlRZDvUpIoG0jU-aC3oZdM0YgXspvw4FcXg1-ad-TbgRjEm_iJ-4KUhAYso6-Y6eimDT_TMhbKBLGE86pUjRPIXVNl6odjkXmcmh7bz9v0-gIfHB9zVbrqE8i8iIwkux-Uhq_VMiIFikjtjmDo5W-7qA_cjvq1dA1QqOJ54ijbdvglUtneuA5CnwfGHAvf94lEPebgP5eCMZjz_Dtl0FTX0u5lOVBs4qeyEfV1XANOq_h1gHmwJDlspGZVuPfSk4-YMCeBy5Ua_gNDqOIQo3EQbmjyN5yD9byFh3WDSQJN1kKT3BZSou-Q8-KT2lu9KT_CpTfVMaNXnPpXJ_H0S9Q7sbUzA6dF1h0Q_mu_bOlWTRlH3vXfZ3_3Vikjk-jhQdaRv8yjTdhX33wAVHk7omKlo76G3QJdnvjysJ6aagFPZ3qhmc",
"e": "AQAB"
}`)
testKeyRSA_4096_2_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "822512e8098b9fa56c9152a56d2607bda912c89f6f4fe1b6ccf41627bf06de29",
"n": "3bcg0diA0TPvK283P02o5UT8cBf5q48uUsfuCkIbLqNFtskGuoft8OJLmik7c7pLNSK3Jad_-xRlfsrcV1fDBs8EJDFnoY-4r_xk_-GQl-OsHzuz7HMvjVYUSYeVZMtzPAfCIFJxSIaiIW9MEwCwYyrtRefedfKUV7Ax6hjOUXtyRZ5DRHj2FnNl6MjE8mTSJ8OQI9ETErg0wF448fJb6hYCidVHfeBSCOceljXMg8a9iDYHVnzsUb9ETl7xlRZDvUpIoG0jU-aC3oZdM0YgXspvw4FcXg1-ad-TbgRjEm_iJ-4KUhAYso6-Y6eimDT_TMhbKBLGE86pUjRPIXVNl6odjkXmcmh7bz9v0-gIfHB9zVbrqE8i8iIwkux-Uhq_VMiIFikjtjmDo5W-7qA_cjvq1dA1QqOJ54ijbdvglUtneuA5CnwfGHAvf94lEPebgP5eCMZjz_Dtl0FTX0u5lOVBs4qeyEfV1XANOq_h1gHmwJDlspGZVuPfSk4-YMCeBy5Ua_gNDqOIQo3EQbmjyN5yD9byFh3WDSQJN1kKT3BZSou-Q8-KT2lu9KT_CpTfVMaNXnPpXJ_H0S9Q7sbUzA6dF1h0Q_mu_bOlWTRlH3vXfZ3_3Vikjk-jhQdaRv8yjTdhX33wAVHk7omKlo76G3QJdnvjysJ6aagFPZ3qhmc",
"e": "AQAB",
"d": "wcO4mAxJUAu-OsxgkR9SusPWhjQ9y5Q_XLNDso1hahng5ES9b6k55mouvlTIk3Q9I_vp6auAKrMBnJS3ilG1rK6hJOxUcBrFwm-m6QV9s3CSzV0E-mEULsYKxtQKWOOBGvaAznSeck7PRL8a0gSpIpGyeYSRo6zTverLRJZXQVjMXlFY4m-ASdCiQJWtoVVBYOUFhHfE3nhECdaOl8xCTcrcfw75AuZXa1ZpIcd0q7m1jGQDd6-HbE3m6UMKiEvD-ZsA68tVs45h0w3ER_pCcfUjRc45Ji1OzEJLezu0RbmoAVOEi4FrxCkB9N_dNn4inD0BhX0axNa4nZH_kfMNUh06081FaBiBq5bNBPQF7lWIYxAhtd2VgsUTZtNxMfHDTllt5fvbCogYngU5-Wj3UM33R_es_aakt_CFndJPmyenP0J7KEBmf4qnlrbxun-jQNV_cbEzgQA_Pt5RIhB5jgofYzT72Ib-lViZUolXpxg8mFQ5BPghqriuaX8eSc85y5rbu-nEvheDkIC7xjTtDBShXMvKKokx6t4d-x_W4sWDuLKsVNn3Veg6mbbl3O74Fr-joEM9unMg-9KCapAuBKmMv4GejOVOIj43i0lm8dsuMslNKsThWpjApE50-VBuySi1f0jD2HNEzjMTt5ZrlNi9bRFzAht46kdc_g4TWLE",
"p": "7cl5QebQldiOrfe2Z7E2aP4d1GIuX4b3u-kXUDa73HI4S2u9TooJF8lfsbrtzJIG60iIzio9Pej8R4ss2_1TPC_F7gQS67ST5yY2zuwUH6jwpt8KtVO3NKd9SNfVJse_mZLp7eAeH8CMq4bqkDig8ZI_Sp1LQtWXvhvhju40i0h1ef-MD36wbc26BE1L6tQMAoJIU-pMDawmWMGG3Q5_LxKHb1cxvV8gVfFgsJbAHMPXxcmpCdp1FqeqpvRs3bch3AMxHwjy1SFRK6lhvliS2ZvD7fdupA09PGJ-cNO3S9iHdCvsnyOHg9pMbxxcYW8Io_t1ihk6JYFvZlDIYvukXw",
"q": "7rKFf6190MlunvlLSdMvN_E2Xuow35_EThaxQ9e_7FS32LKPy847vFreXzVivVW1IVXrRSDgU8VIEyBQzftliLSriefKYdx2jtd_A84baha4p6CAK9SwfZcVEcIuU2Ul8XcUpbg53_bxUWDv4mYewZW_AqaZ0ZR1Ug14gCGHELvjs7MmWkCkJB6lEenRQZvzwcmA8tWzZ6Dlqb_RyLtxcVPbeSuc3t6YxbB1-pquwXoQLB9-XvgR_4L6vuGZrp1D-C0lzboUPoftkC_hKYb_xY6LPJTsL8LUypATcTrAnD0TBrqj5-Oy_4dMin6OcsXrfdxflxaJqkh7B-kz20oa-Q"
}`)
testKeyRSA_4096_3 = mustLoadJWK(`{
"kty": "RSA",
"kid": "ad60df152305985e6bdd8cb1d1f496f766aed097b7464b8b821a09b27f054dcb",
"n": "q8mHub9-A7LH2ykklO52CRQ9KYeM9FlHaoLJCMokmwdb8FF8cAMOPDQ4lx7rB107vwX5oHS6T4uhOUH6ppgZnvB083-f5cBfvZPSwvKg1csOPI3Llp-hIO3vnu7v4M-dXRJevmNdTAoHINXP8mJ1KfGL6BuxckxLEOoIX0suSmyudAE_lc6ioMAfwehkH7qTWwllIuReGwxHHYapM0AFIYysJ0z96l-WNkO3ssLm6Q7txpumM_b0d58OkdezBLujEfn0t5H8hsml_LUSE4TfWcGwm3oDBB2q5tsI2_QmVwdV6BcP43Sm8R7EbJvTQq3YXKLlC-Et4-3CDGaMV6z1eFmHBs_FMGzto-kg9MUFockOe0vkfjHj6AafNw7AFyBJRvV3EdMM-O1ysiC5cs45aowbfQIz_9H9RVbR9iMiEjq6nM34bhgfu-FvOmAcliCOJnDlBy2wWe2XyXTLQVkmn1C_w2mitcQAkP-LiktypUwSIbyDqfBai1_imO1SIVzomRLvzSju5qGrcm0ROV6o9zKXCVc546g9la9FUTTPZsI0_s3hcVRVMaF3fa122hE848serjCqNJ0Nb3CYOlVCc6JrkUdeuCj0Y-on6V6aeCFAQxFb1z_p88STsNJkrcNZp87f1JUaSB4POF01dLNQ2HTe7xJCMRmK-fpNuDCOzNM",
"e": "AQAB"
}`)
testKeyRSA_4096_3_Priv = mustLoadJWK(`{
"kty": "RSA",
"kid": "ad60df152305985e6bdd8cb1d1f496f766aed097b7464b8b821a09b27f054dcb",
"n": "q8mHub9-A7LH2ykklO52CRQ9KYeM9FlHaoLJCMokmwdb8FF8cAMOPDQ4lx7rB107vwX5oHS6T4uhOUH6ppgZnvB083-f5cBfvZPSwvKg1csOPI3Llp-hIO3vnu7v4M-dXRJevmNdTAoHINXP8mJ1KfGL6BuxckxLEOoIX0suSmyudAE_lc6ioMAfwehkH7qTWwllIuReGwxHHYapM0AFIYysJ0z96l-WNkO3ssLm6Q7txpumM_b0d58OkdezBLujEfn0t5H8hsml_LUSE4TfWcGwm3oDBB2q5tsI2_QmVwdV6BcP43Sm8R7EbJvTQq3YXKLlC-Et4-3CDGaMV6z1eFmHBs_FMGzto-kg9MUFockOe0vkfjHj6AafNw7AFyBJRvV3EdMM-O1ysiC5cs45aowbfQIz_9H9RVbR9iMiEjq6nM34bhgfu-FvOmAcliCOJnDlBy2wWe2XyXTLQVkmn1C_w2mitcQAkP-LiktypUwSIbyDqfBai1_imO1SIVzomRLvzSju5qGrcm0ROV6o9zKXCVc546g9la9FUTTPZsI0_s3hcVRVMaF3fa122hE848serjCqNJ0Nb3CYOlVCc6JrkUdeuCj0Y-on6V6aeCFAQxFb1z_p88STsNJkrcNZp87f1JUaSB4POF01dLNQ2HTe7xJCMRmK-fpNuDCOzNM",
"e": "AQAB",
"d": "QFcw8I8aQYRaemlEfEt8BhaAeed9EZ_GscveQ96CK1ZsRuweMU3TrRTaBS_dU1rGH9u7DS_rABQKBIoDuRXKss7Y3sJ0Pvb4ZObSz5VUS_7LjD6HfBi5nr2_O8W-LnNUOyHAPoq0zOAMn221ftEFlPoVLpAAvBB7JRCiph5gbhuak3RMPm2wV4jd3CCQL5oPys8QBCuIW5UTpalkAf_-a_xmFiouB_RZLGXcjaWWGsAuqm5tp5TdJ1h5eoJRWHp2ryrxTzfsXwdzldyzsn_Xr6Rt4y2lp4r9EY4EGW2uVnY25MCOgOCWDkU5yHvselLmcHvKUdK6_11zinV2Jvhuz1FaW10P6lRXxhjuuT4bT7XmY6WkJCUvWf8w_NYrGwDbRvQa6YZcZZWKZ1l2Enkgd_P4qwtrnROQSCe0dJdNG3lSq90lrAwuvb8AhKhpQ8nJSaSQc_h2pa4NJZ-R__9m0_7CRUuEEg9k47AtgdIe09KLSpcACc3W5cBXEw2pL8ihqcf9AVgM0TFQQVrUdIst3YjUiB6r2zLVCRx8KjtT8Pmz6YtMQwDIFTrUopwwai5PEP3QEdxdNF39W1iwkqjA8uw4IlXgZAr4-9s3x617XUL0BQ4TgMpTsEghpV1U8U2HQfYGUKBcAlSqIlAi0MU4QRwcsAJrcoTIi-e34NFMbHE",
"p": "xvZ1BKzaRfrmQT2mmbK1MzKdr4amHrx4EJ_4fRsCRBj8MqB92y4Bp0iuO8rZ3iK_yOOgQQTcr4UP5UEACLOqGDPNa92UUpHu1U5XR3MiIY0kGcdovpkkGU8fq78VRpyofc1b9kvZsy4eL0e7W7jApjGu479D_evnpT7lqSOGRl1Z0QHdI3ctKmKw48-AmQWL4BibXTyaXOfRgTHm4AtbrvFshwCZ43QgzrCrwbhZlOfiIIW9wXa_OqqOhV11BLF72qTglecfTgUOxY0W5GWgbwXCVZ8Lwr6cxYKGrKjmny9UNOIjABGsJjZm5egwDNUhhoaEeXXyzXbCKynvUCDxZw",
"q": "3Qi1bRzj3TYW3Iw9KSDCuuHWBJRO_3Ke-JLB7PHEXc1hsPwF_XDqXPaBbIUtgKaqNzUehYAQbtHHhJVGqZadTKjsNkO4sO_r5nRoxUpFuGgUEmKNN0QeJa_llHzAZ9JpvUrtoyzVZX-2Em_3aAhtV5pZ6t22YToPDjZIBgqL96MQRCyKsv2FS_dbpoYKdXlG6phzkzKPn3oaWkh-VwmgAsFg7uXdiquQEsXYcOq3-77Umiuke6SBbaHLryLflTzOV7YvY7Kw3s3NycS9MDBESKYXqqX3YKvS25ZFFjYbrVOx5AluLiwajZu2biIPZb9rNbSXE77Hll3tAbmA5syJtQ"
}`)
)

199
vendor/github.com/coreos/go-oidc/jwks.go generated vendored Normal file
View file

@ -0,0 +1,199 @@
package oidc
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/pquerna/cachecontrol"
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
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
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
// inflightCtx is the context of the current HTTP request to update the keys.
// Its Err() method returns any errors encountered during that attempt.
//
// If nil, there is no inflight request.
inflightCtx context.Context
// A set of cached keys and their expiry.
cachedKeys []jose.JSONWebKey
expiry time.Time
}
// errContext is a context with a customizable Err() return value.
type errContext struct {
context.Context
cf context.CancelFunc
err error
}
func newErrContext(parent context.Context) *errContext {
ctx, cancel := context.WithCancel(parent)
return &errContext{ctx, cancel, nil}
}
func (e errContext) Err() error {
return e.err
}
// cancel cancels the errContext causing listeners on Done() to return.
func (e errContext) cancel(err error) {
e.err = err
e.cf()
}
func (r *remoteKeySet) keysWithIDFromCache(keyIDs []string) ([]jose.JSONWebKey, bool) {
r.mu.Lock()
keys, expiry := r.cachedKeys, r.expiry
r.mu.Unlock()
// Have the keys expired?
if expiry.Add(keysExpiryDelta).Before(r.now()) {
return nil, false
}
var signingKeys []jose.JSONWebKey
for _, key := range keys {
if contains(keyIDs, key.KeyID) {
signingKeys = append(signingKeys, key)
}
}
if len(signingKeys) == 0 {
// Are the keys about to expire?
if r.now().Add(keysExpiryDelta).After(expiry) {
return nil, false
}
}
return signingKeys, true
}
func (r *remoteKeySet) keysWithID(ctx context.Context, keyIDs []string) ([]jose.JSONWebKey, error) {
keys, ok := r.keysWithIDFromCache(keyIDs)
if ok {
return keys, nil
}
var inflightCtx context.Context
func() {
r.mu.Lock()
defer r.mu.Unlock()
// If there's not a current inflight request, create one.
if r.inflightCtx == nil {
// Use the remoteKeySet's context instead of the requests context
// because a re-sync is unique to the keys set and will span multiple
// requests.
errCtx := newErrContext(r.ctx)
r.inflightCtx = errCtx
go func() {
// TODO(ericchiang): Upstream Kubernetes request that we recover every time
// we spawn a goroutine, because panics in a goroutine will bring down the
// entire program. There's no way to recover from another goroutine's panic.
//
// Most users actually want to let the panic propagate and bring down the
// program because it implies some unrecoverable state.
//
// Add a context key to allow the recover behavior.
//
// See: https://github.com/coreos/go-oidc/issues/89
// Sync keys and close inflightCtx when that's done.
errCtx.cancel(r.updateKeys(r.inflightCtx))
r.mu.Lock()
defer r.mu.Unlock()
r.inflightCtx = nil
}()
}
inflightCtx = r.inflightCtx
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-inflightCtx.Done():
if err := inflightCtx.Err(); err != nil {
return nil, err
}
}
// Since we've just updated keys, we don't care about the cache miss.
keys, _ = r.keysWithIDFromCache(keyIDs)
return keys, nil
}
func (r *remoteKeySet) updateKeys(ctx context.Context) error {
req, err := http.NewRequest("GET", r.jwksURL, nil)
if err != nil {
return fmt.Errorf("oidc: can't create request: %v", err)
}
resp, err := ctxhttp.Do(ctx, clientFromContext(ctx), req)
if err != nil {
return fmt.Errorf("oidc: get keys failed %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("oidc: read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
}
var keySet jose.JSONWebKeySet
if err := json.Unmarshal(body, &keySet); err != nil {
return 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
}
r.mu.Lock()
defer r.mu.Unlock()
r.cachedKeys = keySet.Keys
r.expiry = expiry
return nil
}

99
vendor/github.com/coreos/go-oidc/jwks_test.go generated vendored Normal file
View file

@ -0,0 +1,99 @@
package oidc
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"golang.org/x/net/context"
jose "gopkg.in/square/go-jose.v2"
)
type keyServer struct {
keys jose.JSONWebKeySet
}
func newKeyServer(keys ...jose.JSONWebKey) keyServer {
return keyServer{
keys: jose.JSONWebKeySet{Keys: keys},
}
}
func (k keyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := json.NewEncoder(w).Encode(k.keys); err != nil {
panic(err)
}
}
func TestKeysFormID(t *testing.T) {
tests := []struct {
name string
keys []jose.JSONWebKey
keyIDs []string
wantKeys []jose.JSONWebKey
}{
{
name: "single key",
keys: []jose.JSONWebKey{
testKeyRSA_2048_0,
testKeyECDSA_256_0,
},
keyIDs: []string{
testKeyRSA_2048_0.KeyID,
},
wantKeys: []jose.JSONWebKey{
testKeyRSA_2048_0,
},
},
{
name: "one key id matches",
keys: []jose.JSONWebKey{
testKeyRSA_2048_0,
testKeyECDSA_256_0,
},
keyIDs: []string{
testKeyRSA_2048_0.KeyID,
testKeyRSA_2048_1.KeyID,
},
wantKeys: []jose.JSONWebKey{
testKeyRSA_2048_0,
},
},
{
name: "no valid keys",
keys: []jose.JSONWebKey{
testKeyRSA_2048_1,
testKeyECDSA_256_0,
},
keyIDs: []string{
testKeyRSA_2048_0.KeyID,
},
},
}
t0 := time.Now()
now := func() time.Time { return t0 }
for _, test := range tests {
func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := httptest.NewServer(newKeyServer(test.keys...))
defer server.Close()
keySet := newRemoteKeySet(ctx, server.URL, now)
gotKeys, err := keySet.keysWithID(ctx, test.keyIDs)
if err != nil {
t.Errorf("%s: %v", test.name, err)
return
}
if !reflect.DeepEqual(gotKeys, test.wantKeys) {
t.Errorf("%s: expected keys=%#v, got=%#v", test.name, test.wantKeys, gotKeys)
}
}()
}
}

2
vendor/github.com/coreos/go-oidc/key/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package key is DEPRECATED. Use github.com/coreos/go-oidc instead.
package key

153
vendor/github.com/coreos/go-oidc/key/key.go generated vendored Normal file
View file

@ -0,0 +1,153 @@
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
}

103
vendor/github.com/coreos/go-oidc/key/key_test.go generated vendored Normal file
View file

@ -0,0 +1,103 @@
package key
import (
"crypto/rsa"
"math/big"
"reflect"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
)
func TestPrivateRSAKeyJWK(t *testing.T) {
n := big.NewInt(int64(17))
if n == nil {
panic("NewInt returned nil")
}
k := &PrivateKey{
KeyID: "foo",
PrivateKey: &rsa.PrivateKey{
PublicKey: rsa.PublicKey{N: n, E: 65537},
},
}
want := jose.JWK{
ID: "foo",
Type: "RSA",
Alg: "RS256",
Use: "sig",
Modulus: n,
Exponent: 65537,
}
got := k.JWK()
if !reflect.DeepEqual(want, got) {
t.Fatalf("JWK mismatch: want=%#v got=%#v", want, got)
}
}
func TestPublicKeySetKey(t *testing.T) {
n := big.NewInt(int64(17))
if n == nil {
panic("NewInt returned nil")
}
k := jose.JWK{
ID: "foo",
Type: "RSA",
Alg: "RS256",
Use: "sig",
Modulus: n,
Exponent: 65537,
}
now := time.Now().UTC()
ks := NewPublicKeySet([]jose.JWK{k}, now)
want := &PublicKey{jwk: k}
got := ks.Key("foo")
if !reflect.DeepEqual(want, got) {
t.Errorf("Unexpected response from PublicKeySet.Key: want=%#v got=%#v", want, got)
}
got = ks.Key("bar")
if got != nil {
t.Errorf("Expected nil response from PublicKeySet.Key, got %#v", got)
}
}
func TestPublicKeyMarshalJSON(t *testing.T) {
k := jose.JWK{
ID: "foo",
Type: "RSA",
Alg: "RS256",
Use: "sig",
Modulus: big.NewInt(int64(17)),
Exponent: 65537,
}
want := `{"kid":"foo","kty":"RSA","alg":"RS256","use":"sig","e":"AQAB","n":"EQ=="}`
pubKey := NewPublicKey(k)
gotBytes, err := pubKey.MarshalJSON()
if err != nil {
t.Fatalf("failed to marshal public key: %v", err)
}
got := string(gotBytes)
if got != want {
t.Errorf("got != want:\n%s\n%s", got, want)
}
}
func TestGeneratePrivateKeyIDs(t *testing.T) {
key1, err := GeneratePrivateKey()
if err != nil {
t.Fatalf("GeneratePrivateKey(): %v", err)
}
key2, err := GeneratePrivateKey()
if err != nil {
t.Fatalf("GeneratePrivateKey(): %v", err)
}
if key1.KeyID == key2.KeyID {
t.Fatalf("expected different keys to have different key IDs")
}
}

99
vendor/github.com/coreos/go-oidc/key/manager.go generated vendored Normal file
View file

@ -0,0 +1,99 @@
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
}

225
vendor/github.com/coreos/go-oidc/key/manager_test.go generated vendored Normal file
View file

@ -0,0 +1,225 @@
package key
import (
"crypto/rsa"
"math/big"
"reflect"
"strconv"
"testing"
"time"
"github.com/jonboulle/clockwork"
"github.com/coreos/go-oidc/jose"
)
var (
jwk1 jose.JWK
jwk2 jose.JWK
jwk3 jose.JWK
)
func init() {
jwk1 = jose.JWK{
ID: "1",
Type: "RSA",
Alg: "RS256",
Use: "sig",
Modulus: big.NewInt(1),
Exponent: 65537,
}
jwk2 = jose.JWK{
ID: "2",
Type: "RSA",
Alg: "RS256",
Use: "sig",
Modulus: big.NewInt(2),
Exponent: 65537,
}
jwk3 = jose.JWK{
ID: "3",
Type: "RSA",
Alg: "RS256",
Use: "sig",
Modulus: big.NewInt(3),
Exponent: 65537,
}
}
func generatePrivateKeyStatic(t *testing.T, idAndN int) *PrivateKey {
n := big.NewInt(int64(idAndN))
if n == nil {
t.Fatalf("Call to NewInt(%d) failed", idAndN)
}
pk := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{N: n, E: 65537},
}
return &PrivateKey{
KeyID: strconv.Itoa(idAndN),
PrivateKey: pk,
}
}
func TestPrivateKeyManagerJWKsRotate(t *testing.T) {
k1 := generatePrivateKeyStatic(t, 1)
k2 := generatePrivateKeyStatic(t, 2)
k3 := generatePrivateKeyStatic(t, 3)
km := NewPrivateKeyManager()
err := km.Set(&PrivateKeySet{
keys: []*PrivateKey{k1, k2, k3},
ActiveKeyID: k1.KeyID,
expiresAt: time.Now().Add(time.Minute),
})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want := []jose.JWK{jwk1, jwk2, jwk3}
got, err := km.JWKs()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(want, got) {
t.Fatalf("JWK mismatch: want=%#v got=%#v", want, got)
}
}
func TestPrivateKeyManagerSigner(t *testing.T) {
k := generatePrivateKeyStatic(t, 13)
km := NewPrivateKeyManager()
err := km.Set(&PrivateKeySet{
keys: []*PrivateKey{k},
ActiveKeyID: k.KeyID,
expiresAt: time.Now().Add(time.Minute),
})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
signer, err := km.Signer()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
wantID := "13"
gotID := signer.ID()
if wantID != gotID {
t.Fatalf("Signer has incorrect ID: want=%s got=%s", wantID, gotID)
}
}
func TestPrivateKeyManagerHealthyFail(t *testing.T) {
keyFixture := generatePrivateKeyStatic(t, 1)
tests := []*privateKeyManager{
// keySet nil
&privateKeyManager{
keySet: nil,
clock: clockwork.NewRealClock(),
},
// zero keys
&privateKeyManager{
keySet: &PrivateKeySet{
keys: []*PrivateKey{},
expiresAt: time.Now().Add(time.Minute),
},
clock: clockwork.NewRealClock(),
},
// key set expired
&privateKeyManager{
keySet: &PrivateKeySet{
keys: []*PrivateKey{keyFixture},
expiresAt: time.Now().Add(-1 * time.Minute),
},
clock: clockwork.NewRealClock(),
},
}
for i, tt := range tests {
if err := tt.Healthy(); err == nil {
t.Errorf("case %d: nil error", i)
}
}
}
func TestPrivateKeyManagerHealthyFailsOtherMethods(t *testing.T) {
km := NewPrivateKeyManager()
if _, err := km.JWKs(); err == nil {
t.Fatalf("Expected non-nil error")
}
if _, err := km.Signer(); err == nil {
t.Fatalf("Expected non-nil error")
}
}
func TestPrivateKeyManagerExpiresAt(t *testing.T) {
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
k := generatePrivateKeyStatic(t, 17)
km := &privateKeyManager{
clock: fc,
}
want := fc.Now().UTC()
got := km.ExpiresAt()
if want != got {
t.Fatalf("Incorrect expiration time: want=%v got=%v", want, got)
}
err := km.Set(&PrivateKeySet{
keys: []*PrivateKey{k},
ActiveKeyID: k.KeyID,
expiresAt: now.Add(2 * time.Minute),
})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want = fc.Now().UTC().Add(2 * time.Minute)
got = km.ExpiresAt()
if want != got {
t.Fatalf("Incorrect expiration time: want=%v got=%v", want, got)
}
}
func TestPublicKeys(t *testing.T) {
km := NewPrivateKeyManager()
k1 := generatePrivateKeyStatic(t, 1)
k2 := generatePrivateKeyStatic(t, 2)
k3 := generatePrivateKeyStatic(t, 3)
tests := [][]*PrivateKey{
[]*PrivateKey{k1},
[]*PrivateKey{k1, k2},
[]*PrivateKey{k1, k2, k3},
}
for i, tt := range tests {
ks := &PrivateKeySet{
keys: tt,
expiresAt: time.Now().Add(time.Hour),
}
km.Set(ks)
jwks, err := km.JWKs()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
pks := NewPublicKeySet(jwks, time.Now().Add(time.Hour))
want := pks.Keys()
got, err := km.PublicKeys()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(want, got) {
t.Errorf("case %d: Invalid public keys: want=%v got=%v", i, want, got)
}
}
}

55
vendor/github.com/coreos/go-oidc/key/repo.go generated vendored Normal file
View file

@ -0,0 +1,55 @@
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
}

159
vendor/github.com/coreos/go-oidc/key/rotate.go generated vendored Normal file
View file

@ -0,0 +1,159 @@
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))
}

311
vendor/github.com/coreos/go-oidc/key/rotate_test.go generated vendored Normal file
View file

@ -0,0 +1,311 @@
package key
import (
"reflect"
"testing"
"time"
"github.com/jonboulle/clockwork"
)
func generatePrivateKeySerialFunc(t *testing.T) GeneratePrivateKeyFunc {
var n int
return func() (*PrivateKey, error) {
n++
return generatePrivateKeyStatic(t, n), nil
}
}
func TestRotate(t *testing.T) {
now := time.Now()
k1 := generatePrivateKeyStatic(t, 1)
k2 := generatePrivateKeyStatic(t, 2)
k3 := generatePrivateKeyStatic(t, 3)
tests := []struct {
start *PrivateKeySet
key *PrivateKey
keep int
exp time.Time
want *PrivateKeySet
}{
// start with nil keys
{
start: nil,
key: k1,
keep: 2,
exp: now.Add(time.Second),
want: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(time.Second),
},
},
// start with zero keys
{
start: &PrivateKeySet{},
key: k1,
keep: 2,
exp: now.Add(time.Second),
want: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(time.Second),
},
},
// add second key
{
start: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now,
},
key: k2,
keep: 2,
exp: now.Add(time.Second),
want: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(time.Second),
},
},
// rotate in third key
{
start: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now,
},
key: k3,
keep: 2,
exp: now.Add(time.Second),
want: &PrivateKeySet{
keys: []*PrivateKey{k3, k2},
ActiveKeyID: k3.KeyID,
expiresAt: now.Add(time.Second),
},
},
}
for i, tt := range tests {
repo := NewPrivateKeySetRepo()
if tt.start != nil {
err := repo.Set(tt.start)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
}
rotatePrivateKeys(repo, tt.key, tt.keep, tt.exp)
got, err := repo.Get()
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("case %d: unexpected result: want=%#v got=%#v", i, tt.want, got)
}
}
}
func TestPrivateKeyRotatorRun(t *testing.T) {
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
k1 := generatePrivateKeyStatic(t, 1)
k2 := generatePrivateKeyStatic(t, 2)
k3 := generatePrivateKeyStatic(t, 3)
k4 := generatePrivateKeyStatic(t, 4)
kRepo := NewPrivateKeySetRepo()
krot := NewPrivateKeyRotator(kRepo, 4*time.Second)
krot.clock = fc
krot.generateKey = generatePrivateKeySerialFunc(t)
steps := []*PrivateKeySet{
&PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(4 * time.Second),
},
&PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(6 * time.Second),
},
&PrivateKeySet{
keys: []*PrivateKey{k3, k2},
ActiveKeyID: k3.KeyID,
expiresAt: now.Add(8 * time.Second),
},
&PrivateKeySet{
keys: []*PrivateKey{k4, k3},
ActiveKeyID: k4.KeyID,
expiresAt: now.Add(10 * time.Second),
},
}
stop := krot.Run()
defer close(stop)
for i, st := range steps {
// wait for the rotater to get sleepy
fc.BlockUntil(1)
got, err := kRepo.Get()
if err != nil {
t.Fatalf("step %d: unexpected error: %v", i, err)
}
if !reflect.DeepEqual(st, got) {
t.Fatalf("step %d: unexpected state: want=%#v got=%#v", i, st, got)
}
fc.Advance(2 * time.Second)
}
}
func TestPrivateKeyRotatorExpiresAt(t *testing.T) {
fc := clockwork.NewFakeClock()
krot := &PrivateKeyRotator{
clock: fc,
ttl: time.Minute,
}
got := krot.expiresAt()
want := fc.Now().UTC().Add(time.Minute)
if !reflect.DeepEqual(want, got) {
t.Errorf("Incorrect expiration time: want=%v got=%v", want, got)
}
}
func TestNextRotation(t *testing.T) {
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
tests := []struct {
expiresAt time.Time
ttl time.Duration
numKeys int
expected time.Duration
}{
{
// closest to prod
expiresAt: now.Add(time.Hour * 24),
ttl: time.Hour * 24,
numKeys: 2,
expected: time.Hour * 12,
},
{
expiresAt: now.Add(time.Hour * 2),
ttl: time.Hour * 4,
numKeys: 2,
expected: 0,
},
{
// No keys.
expiresAt: now.Add(time.Hour * 2),
ttl: time.Hour * 4,
numKeys: 0,
expected: 0,
},
{
// Nil keyset.
expiresAt: now.Add(time.Hour * 2),
ttl: time.Hour * 4,
numKeys: -1,
expected: 0,
},
{
// KeySet expired.
expiresAt: now.Add(time.Hour * -2),
ttl: time.Hour * 4,
numKeys: 2,
expected: 0,
},
{
// Expiry past now + TTL
expiresAt: now.Add(time.Hour * 5),
ttl: time.Hour * 4,
numKeys: 2,
expected: 3 * time.Hour,
},
}
for i, tt := range tests {
kRepo := NewPrivateKeySetRepo()
krot := NewPrivateKeyRotator(kRepo, tt.ttl)
krot.clock = fc
pks := &PrivateKeySet{
expiresAt: tt.expiresAt,
}
if tt.numKeys != -1 {
for n := 0; n < tt.numKeys; n++ {
pks.keys = append(pks.keys, generatePrivateKeyStatic(t, n))
}
err := kRepo.Set(pks)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
}
actual, err := krot.nextRotation()
if err != nil {
t.Errorf("case %d: error calling shouldRotate(): %v", i, err)
}
if actual != tt.expected {
t.Errorf("case %d: actual == %v, want %v", i, actual, tt.expected)
}
}
}
func TestHealthy(t *testing.T) {
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
tests := []struct {
expiresAt time.Time
numKeys int
expected error
}{
{
expiresAt: now.Add(time.Hour),
numKeys: 2,
expected: nil,
},
{
expiresAt: now.Add(time.Hour),
numKeys: -1,
expected: ErrorNoKeys,
},
{
expiresAt: now.Add(time.Hour),
numKeys: 0,
expected: ErrorNoKeys,
},
{
expiresAt: now.Add(-time.Hour),
numKeys: 2,
expected: ErrorPrivateKeysExpired,
},
}
for i, tt := range tests {
kRepo := NewPrivateKeySetRepo()
krot := NewPrivateKeyRotator(kRepo, time.Hour)
krot.clock = fc
pks := &PrivateKeySet{
expiresAt: tt.expiresAt,
}
if tt.numKeys != -1 {
for n := 0; n < tt.numKeys; n++ {
pks.keys = append(pks.keys, generatePrivateKeyStatic(t, n))
}
err := kRepo.Set(pks)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}
}
if err := krot.Healthy(); err != tt.expected {
t.Errorf("case %d: got==%q, want==%q", i, err, tt.expected)
}
}
}

91
vendor/github.com/coreos/go-oidc/key/sync.go generated vendored Normal file
View file

@ -0,0 +1,91 @@
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
}

214
vendor/github.com/coreos/go-oidc/key/sync_test.go generated vendored Normal file
View file

@ -0,0 +1,214 @@
package key
import (
"errors"
"reflect"
"sync"
"testing"
"time"
"github.com/jonboulle/clockwork"
)
type staticReadableKeySetRepo struct {
mu sync.RWMutex
ks KeySet
err error
}
func (r *staticReadableKeySetRepo) Get() (KeySet, error) {
r.mu.RLock()
defer r.mu.RUnlock()
return r.ks, r.err
}
func (r *staticReadableKeySetRepo) set(ks KeySet, err error) {
r.mu.Lock()
defer r.mu.Unlock()
r.ks, r.err = ks, err
}
func TestKeySyncerSync(t *testing.T) {
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
k1 := generatePrivateKeyStatic(t, 1)
k2 := generatePrivateKeyStatic(t, 2)
k3 := generatePrivateKeyStatic(t, 3)
steps := []struct {
fromKS KeySet
fromErr error
advance time.Duration
want *PrivateKeySet
}{
// on startup, first sync should trigger within a second
{
fromKS: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(10 * time.Second),
},
advance: time.Second,
want: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(10 * time.Second),
},
},
// advance halfway into TTL, triggering sync
{
fromKS: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(15 * time.Second),
},
advance: 5 * time.Second,
want: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(15 * time.Second),
},
},
// advance halfway into TTL, triggering sync that fails
{
fromErr: errors.New("fail!"),
advance: 10 * time.Second,
want: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(15 * time.Second),
},
},
// sync retries quickly, and succeeds with fixed data
{
fromKS: &PrivateKeySet{
keys: []*PrivateKey{k3, k2, k1},
ActiveKeyID: k3.KeyID,
expiresAt: now.Add(25 * time.Second),
},
advance: 3 * time.Second,
want: &PrivateKeySet{
keys: []*PrivateKey{k3, k2, k1},
ActiveKeyID: k3.KeyID,
expiresAt: now.Add(25 * time.Second),
},
},
}
from := &staticReadableKeySetRepo{}
to := NewPrivateKeySetRepo()
syncer := NewKeySetSyncer(from, to)
syncer.clock = fc
stop := syncer.Run()
defer close(stop)
for i, st := range steps {
from.set(st.fromKS, st.fromErr)
fc.Advance(st.advance)
fc.BlockUntil(1)
ks, err := to.Get()
if err != nil {
t.Fatalf("step %d: unable to get keys: %v", i, err)
}
if !reflect.DeepEqual(st.want, ks) {
t.Fatalf("step %d: incorrect state: want=%#v got=%#v", i, st.want, ks)
}
}
}
func TestSync(t *testing.T) {
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
k1 := generatePrivateKeyStatic(t, 1)
k2 := generatePrivateKeyStatic(t, 2)
k3 := generatePrivateKeyStatic(t, 3)
tests := []struct {
keySet *PrivateKeySet
want time.Duration
}{
{
keySet: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(time.Minute),
},
want: time.Minute,
},
{
keySet: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(time.Minute),
},
want: time.Minute,
},
{
keySet: &PrivateKeySet{
keys: []*PrivateKey{k3, k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(time.Minute),
},
want: time.Minute,
},
{
keySet: &PrivateKeySet{
keys: []*PrivateKey{k2, k1},
ActiveKeyID: k2.KeyID,
expiresAt: now.Add(time.Hour),
},
want: time.Hour,
},
{
keySet: &PrivateKeySet{
keys: []*PrivateKey{k1},
ActiveKeyID: k1.KeyID,
expiresAt: now.Add(-time.Hour),
},
want: 0,
},
}
for i, tt := range tests {
from := NewPrivateKeySetRepo()
to := NewPrivateKeySetRepo()
err := from.Set(tt.keySet)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
exp, err := syncKeySet(from, to, fc)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
if tt.want != exp {
t.Errorf("case %d: want=%v got=%v", i, tt.want, exp)
}
}
}
func TestSyncFail(t *testing.T) {
tests := []error{
nil,
errors.New("fail!"),
}
for i, tt := range tests {
from := &staticReadableKeySetRepo{ks: nil, err: tt}
to := NewPrivateKeySetRepo()
if _, err := syncKeySet(from, to, clockwork.NewFakeClock()); err == nil {
t.Errorf("case %d: expected non-nil error", i)
}
}
}

2
vendor/github.com/coreos/go-oidc/oauth2/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package oauth2 is DEPRECATED. Use golang.org/x/oauth instead.
package oauth2

29
vendor/github.com/coreos/go-oidc/oauth2/error.go generated vendored Normal file
View file

@ -0,0 +1,29 @@
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}
}

416
vendor/github.com/coreos/go-oidc/oauth2/oauth2.go generated vendored Normal file
View file

@ -0,0 +1,416 @@
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
}

518
vendor/github.com/coreos/go-oidc/oauth2/oauth2_test.go generated vendored Normal file
View file

@ -0,0 +1,518 @@
package oauth2
import (
"errors"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
phttp "github.com/coreos/go-oidc/http"
)
func TestResponseTypesEqual(t *testing.T) {
tests := []struct {
r1, r2 string
want bool
}{
{"code", "code", true},
{"id_token", "code", false},
{"code token", "token code", true},
{"code token", "code token", true},
{"foo", "bar code", false},
{"code token id_token", "token id_token code", true},
{"code token id_token", "token id_token code zoo", false},
}
for i, tt := range tests {
got1 := ResponseTypesEqual(tt.r1, tt.r2)
got2 := ResponseTypesEqual(tt.r2, tt.r1)
if got1 != got2 {
t.Errorf("case %d: got different answers with different orders", i)
}
if tt.want != got1 {
t.Errorf("case %d: want=%t, got=%t", i, tt.want, got1)
}
}
}
func TestParseAuthCodeRequest(t *testing.T) {
tests := []struct {
query url.Values
wantACR AuthCodeRequest
wantErr error
}{
// no redirect_uri
{
query: url.Values{
"response_type": []string{"code"},
"scope": []string{"foo bar baz"},
"client_id": []string{"XXX"},
"state": []string{"pants"},
},
wantACR: AuthCodeRequest{
ResponseType: "code",
ClientID: "XXX",
Scope: []string{"foo", "bar", "baz"},
State: "pants",
RedirectURL: nil,
},
},
// with redirect_uri
{
query: url.Values{
"response_type": []string{"code"},
"redirect_uri": []string{"https://127.0.0.1:5555/callback?foo=bar"},
"scope": []string{"foo bar baz"},
"client_id": []string{"XXX"},
"state": []string{"pants"},
},
wantACR: AuthCodeRequest{
ResponseType: "code",
ClientID: "XXX",
Scope: []string{"foo", "bar", "baz"},
State: "pants",
RedirectURL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:5555",
Path: "/callback",
RawQuery: "foo=bar",
},
},
},
// unsupported response_type doesn't trigger error
{
query: url.Values{
"response_type": []string{"token"},
"redirect_uri": []string{"https://127.0.0.1:5555/callback?foo=bar"},
"scope": []string{"foo bar baz"},
"client_id": []string{"XXX"},
"state": []string{"pants"},
},
wantACR: AuthCodeRequest{
ResponseType: "token",
ClientID: "XXX",
Scope: []string{"foo", "bar", "baz"},
State: "pants",
RedirectURL: &url.URL{
Scheme: "https",
Host: "127.0.0.1:5555",
Path: "/callback",
RawQuery: "foo=bar",
},
},
},
// unparseable redirect_uri
{
query: url.Values{
"response_type": []string{"code"},
"redirect_uri": []string{":"},
"scope": []string{"foo bar baz"},
"client_id": []string{"XXX"},
"state": []string{"pants"},
},
wantACR: AuthCodeRequest{
ResponseType: "code",
ClientID: "XXX",
Scope: []string{"foo", "bar", "baz"},
State: "pants",
},
wantErr: NewError(ErrorInvalidRequest),
},
// no client_id, redirect_uri not parsed
{
query: url.Values{
"response_type": []string{"code"},
"redirect_uri": []string{"https://127.0.0.1:5555/callback?foo=bar"},
"scope": []string{"foo bar baz"},
"client_id": []string{},
"state": []string{"pants"},
},
wantACR: AuthCodeRequest{
ResponseType: "code",
ClientID: "",
Scope: []string{"foo", "bar", "baz"},
State: "pants",
RedirectURL: nil,
},
wantErr: NewError(ErrorInvalidRequest),
},
}
for i, tt := range tests {
got, err := ParseAuthCodeRequest(tt.query)
if !reflect.DeepEqual(tt.wantErr, err) {
t.Errorf("case %d: incorrect error value: want=%q got=%q", i, tt.wantErr, err)
}
if !reflect.DeepEqual(tt.wantACR, got) {
t.Errorf("case %d: incorrect AuthCodeRequest value: want=%#v got=%#v", i, tt.wantACR, got)
}
}
}
type fakeBadClient struct {
Request *http.Request
err error
}
func (f *fakeBadClient) Do(r *http.Request) (*http.Response, error) {
f.Request = r
return nil, f.err
}
func TestClientCredsToken(t *testing.T) {
hc := &fakeBadClient{nil, errors.New("error")}
cfg := Config{
Credentials: ClientCredentials{ID: "c#id", Secret: "c secret"},
Scope: []string{"foo-scope", "bar-scope"},
TokenURL: "http://example.com/token",
AuthMethod: AuthMethodClientSecretBasic,
RedirectURL: "http://example.com/redirect",
AuthURL: "http://example.com/auth",
}
c, err := NewClient(hc, cfg)
if err != nil {
t.Errorf("unexpected error %v", err)
}
scope := []string{"openid"}
c.ClientCredsToken(scope)
if hc.Request == nil {
t.Error("request is empty")
}
tu := hc.Request.URL.String()
if cfg.TokenURL != tu {
t.Errorf("wrong token url, want=%v, got=%v", cfg.TokenURL, tu)
}
ct := hc.Request.Header.Get("Content-Type")
if ct != "application/x-www-form-urlencoded" {
t.Errorf("wrong content-type, want=application/x-www-form-urlencoded, got=%v", ct)
}
cid, secret, ok := phttp.BasicAuth(hc.Request)
if !ok {
t.Error("unexpected error parsing basic auth")
}
if url.QueryEscape(cfg.Credentials.ID) != cid {
t.Errorf("wrong client ID, want=%v, got=%v", cfg.Credentials.ID, cid)
}
if url.QueryEscape(cfg.Credentials.Secret) != secret {
t.Errorf("wrong client secret, want=%v, got=%v", cfg.Credentials.Secret, secret)
}
err = hc.Request.ParseForm()
if err != nil {
t.Error("unexpected error parsing form")
}
gt := hc.Request.PostForm.Get("grant_type")
if gt != GrantTypeClientCreds {
t.Errorf("wrong grant_type, want=%v, got=%v", GrantTypeClientCreds, gt)
}
sc := strings.Split(hc.Request.PostForm.Get("scope"), " ")
if !reflect.DeepEqual(scope, sc) {
t.Errorf("wrong scope, want=%v, got=%v", scope, sc)
}
}
func TestUserCredsToken(t *testing.T) {
hc := &fakeBadClient{nil, errors.New("error")}
cfg := Config{
Credentials: ClientCredentials{ID: "c#id", Secret: "c secret"},
Scope: []string{"foo-scope", "bar-scope"},
TokenURL: "http://example.com/token",
AuthMethod: AuthMethodClientSecretBasic,
RedirectURL: "http://example.com/redirect",
AuthURL: "http://example.com/auth",
}
c, err := NewClient(hc, cfg)
if err != nil {
t.Errorf("unexpected error %v", err)
}
c.UserCredsToken("username", "password")
if hc.Request == nil {
t.Error("request is empty")
}
tu := hc.Request.URL.String()
if cfg.TokenURL != tu {
t.Errorf("wrong token url, want=%v, got=%v", cfg.TokenURL, tu)
}
ct := hc.Request.Header.Get("Content-Type")
if ct != "application/x-www-form-urlencoded" {
t.Errorf("wrong content-type, want=application/x-www-form-urlencoded, got=%v", ct)
}
cid, secret, ok := phttp.BasicAuth(hc.Request)
if !ok {
t.Error("unexpected error parsing basic auth")
}
if url.QueryEscape(cfg.Credentials.ID) != cid {
t.Errorf("wrong client ID, want=%v, got=%v", cfg.Credentials.ID, cid)
}
if url.QueryEscape(cfg.Credentials.Secret) != secret {
t.Errorf("wrong client secret, want=%v, got=%v", cfg.Credentials.Secret, secret)
}
err = hc.Request.ParseForm()
if err != nil {
t.Error("unexpected error parsing form")
}
gt := hc.Request.PostForm.Get("grant_type")
if gt != GrantTypeUserCreds {
t.Errorf("wrong grant_type, want=%v, got=%v", GrantTypeUserCreds, gt)
}
sc := strings.Split(hc.Request.PostForm.Get("scope"), " ")
if !reflect.DeepEqual(c.scope, sc) {
t.Errorf("wrong scope, want=%v, got=%v", c.scope, sc)
}
}
func TestNewAuthenticatedRequest(t *testing.T) {
tests := []struct {
authMethod string
url string
values url.Values
}{
{
authMethod: AuthMethodClientSecretBasic,
url: "http://example.com/token",
values: url.Values{},
},
{
authMethod: AuthMethodClientSecretPost,
url: "http://example.com/token",
values: url.Values{},
},
}
for i, tt := range tests {
cfg := Config{
Credentials: ClientCredentials{ID: "c#id", Secret: "c secret"},
Scope: []string{"foo-scope", "bar-scope"},
TokenURL: "http://example.com/token",
AuthURL: "http://example.com/auth",
RedirectURL: "http://example.com/redirect",
AuthMethod: tt.authMethod,
}
c, err := NewClient(nil, cfg)
req, err := c.newAuthenticatedRequest(tt.url, tt.values)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
err = req.ParseForm()
if err != nil {
t.Errorf("case %d: want nil err, got %v", i, err)
}
if tt.authMethod == AuthMethodClientSecretBasic {
cid, secret, ok := phttp.BasicAuth(req)
if !ok {
t.Errorf("case %d: !ok parsing Basic Auth headers", i)
continue
}
if cid != url.QueryEscape(cfg.Credentials.ID) {
t.Errorf("case %d: want CID == %q, got CID == %q", i, cfg.Credentials.ID, cid)
}
if secret != url.QueryEscape(cfg.Credentials.Secret) {
t.Errorf("case %d: want secret == %q, got secret == %q", i, cfg.Credentials.Secret, secret)
}
} else if tt.authMethod == AuthMethodClientSecretPost {
if req.PostFormValue("client_secret") != cfg.Credentials.Secret {
t.Errorf("case %d: want client_secret == %q, got client_secret == %q",
i, cfg.Credentials.Secret, req.PostFormValue("client_secret"))
}
}
for k, v := range tt.values {
if !reflect.DeepEqual(v, req.PostForm[k]) {
t.Errorf("case %d: key:%q want==%q, got==%q", i, k, v, req.PostForm[k])
}
}
if req.URL.String() != tt.url {
t.Errorf("case %d: want URL==%q, got URL==%q", i, tt.url, req.URL.String())
}
}
}
func TestParseTokenResponse(t *testing.T) {
type response struct {
body string
contentType string
statusCode int // defaults to http.StatusOK
}
tests := []struct {
resp response
wantResp TokenResponse
wantError *Error
}{
{
resp: response{
body: "{ \"error\": \"invalid_client\", \"state\": \"foo\" }",
contentType: "application/json",
statusCode: http.StatusBadRequest,
},
wantError: &Error{Type: "invalid_client", State: "foo"},
},
{
resp: response{
body: "{ \"error\": \"invalid_request\", \"state\": \"bar\" }",
contentType: "application/json",
statusCode: http.StatusBadRequest,
},
wantError: &Error{Type: "invalid_request", State: "bar"},
},
{
// Actual response from bitbucket
resp: response{
body: `{"error_description": "Invalid OAuth client credentials", "error": "unauthorized_client"}`,
contentType: "application/json",
statusCode: http.StatusBadRequest,
},
wantError: &Error{Type: "unauthorized_client", Description: "Invalid OAuth client credentials"},
},
{
// Actual response from github
resp: response{
body: `error=incorrect_client_credentials&error_description=The+client_id+and%2For+client_secret+passed+are+incorrect.&error_uri=https%3A%2F%2Fdeveloper.github.com%2Fv3%2Foauth%2F%23incorrect-client-credentials`,
contentType: "application/x-www-form-urlencoded; charset=utf-8",
},
wantError: &Error{Type: "incorrect_client_credentials", Description: "The client_id and/or client_secret passed are incorrect."},
},
{
resp: response{
body: `{"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"}`,
contentType: "application/json",
},
wantResp: TokenResponse{
AccessToken: "e72e16c7e42f292c6912e7710c838347ae178b4a",
TokenType: "bearer",
Scope: "repo,gist",
},
},
{
resp: response{
body: `access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer`,
contentType: "application/x-www-form-urlencoded",
},
wantResp: TokenResponse{
AccessToken: "e72e16c7e42f292c6912e7710c838347ae178b4a",
TokenType: "bearer",
Scope: "user,gist",
},
},
{
resp: response{
body: `{"access_token":"foo","id_token":"bar","expires_in":200,"token_type":"bearer","refresh_token":"spam"}`,
contentType: "application/json; charset=utf-8",
},
wantResp: TokenResponse{
AccessToken: "foo",
IDToken: "bar",
Expires: 200,
TokenType: "bearer",
RefreshToken: "spam",
},
},
{
// Azure AD returns "expires_in" value as string
resp: response{
body: `{"access_token":"foo","id_token":"bar","expires_in":"300","token_type":"bearer","refresh_token":"spam"}`,
contentType: "application/json; charset=utf-8",
},
wantResp: TokenResponse{
AccessToken: "foo",
IDToken: "bar",
Expires: 300,
TokenType: "bearer",
RefreshToken: "spam",
},
},
{
resp: response{
body: `{"access_token":"foo","id_token":"bar","expires":200,"token_type":"bearer","refresh_token":"spam"}`,
contentType: "application/json; charset=utf-8",
},
wantResp: TokenResponse{
AccessToken: "foo",
IDToken: "bar",
Expires: 200,
TokenType: "bearer",
RefreshToken: "spam",
},
},
{
resp: response{
body: `access_token=foo&id_token=bar&expires_in=200&token_type=bearer&refresh_token=spam`,
contentType: "application/x-www-form-urlencoded",
},
wantResp: TokenResponse{
AccessToken: "foo",
IDToken: "bar",
Expires: 200,
TokenType: "bearer",
RefreshToken: "spam",
},
},
}
for i, tt := range tests {
r := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{tt.resp.contentType},
"Content-Length": []string{strconv.Itoa(len([]byte(tt.resp.body)))},
},
Body: ioutil.NopCloser(strings.NewReader(tt.resp.body)),
ContentLength: int64(len([]byte(tt.resp.body))),
}
if tt.resp.statusCode != 0 {
r.StatusCode = tt.resp.statusCode
}
result, err := parseTokenResponse(r)
if err != nil {
if tt.wantError == nil {
t.Errorf("case %d: got error==%v", i, err)
continue
}
if !reflect.DeepEqual(tt.wantError, err) {
t.Errorf("case %d: want=%+v, got=%+v", i, tt.wantError, err)
}
} else {
if tt.wantError != nil {
t.Errorf("case %d: want error==%v, got==nil", i, tt.wantError)
continue
}
// don't compare the raw body (it's really big and clogs error messages)
result.RawBody = tt.wantResp.RawBody
if !reflect.DeepEqual(tt.wantResp, result) {
t.Errorf("case %d: want=%+v, got=%+v", i, tt.wantResp, result)
}
}
}
}

View file

@ -1,3 +1,4 @@
// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
package oidc package oidc
import ( import (
@ -11,13 +12,7 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) jose "gopkg.in/square/go-jose.v2"
var (
// ErrTokenExpired indicates that a token parsed by a verifier has expired.
ErrTokenExpired = errors.New("oidc: ID Token expired")
// ErrNotSupported indicates that the requested optional OpenID Connect endpoint is not supported by the provider.
ErrNotSupported = errors.New("oidc: endpoint not supported")
) )
const ( const (
@ -35,23 +30,61 @@ const (
ScopeOfflineAccess = "offline_access" ScopeOfflineAccess = "offline_access"
) )
// Provider contains the subset of the OpenID Connect provider metadata needed to request // ClientContext returns a new Context that carries the provided HTTP client.
// and verify ID Tokens. //
// 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 clientFromContext(ctx context.Context) *http.Client {
if client, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
return client
}
return http.DefaultClient
}
// Provider represents an OpenID Connect server's configuration.
type Provider struct { type Provider struct {
issuer string
authURL string
tokenURL string
userInfoURL string
// Raw claims returned by the server.
rawClaims []byte
remoteKeySet *remoteKeySet
}
type cachedKeys struct {
keys []jose.JSONWebKey
expiry time.Time
}
type providerJSON struct {
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"` AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"` TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"` JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"` UserInfoURL string `json:"userinfo_endpoint"`
// Raw claims returned by the server.
rawClaims []byte
} }
// NewProvider uses the OpenID Connect disovery mechanism to construct a Provider. // 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) { func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
resp, err := contextClient(ctx).Get(wellKnown) resp, err := clientFromContext(ctx).Get(wellKnown)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,18 +96,36 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
return nil, fmt.Errorf("%s: %s", resp.Status, body) return nil, fmt.Errorf("%s: %s", resp.Status, body)
} }
defer resp.Body.Close() defer resp.Body.Close()
var p Provider var p providerJSON
if err := json.Unmarshal(body, &p); err != nil { if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err) return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
} }
p.rawClaims = body
if p.Issuer != issuer { 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 nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
} }
return &p, nil return &Provider{
issuer: p.Issuer,
authURL: p.AuthURL,
tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL,
rawClaims: body,
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now),
}, nil
} }
// Claims returns additional fields returned by the server during discovery. // 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 { func (p *Provider) Claims(v interface{}) error {
if p.rawClaims == nil { if p.rawClaims == nil {
return errors.New("oidc: claims not set") return errors.New("oidc: claims not set")
@ -84,7 +135,7 @@ func (p *Provider) Claims(v interface{}) error {
// Endpoint returns the OAuth2 auth and token endpoints for the given provider. // Endpoint returns the OAuth2 auth and token endpoints for the given provider.
func (p *Provider) Endpoint() oauth2.Endpoint { func (p *Provider) Endpoint() oauth2.Endpoint {
return oauth2.Endpoint{AuthURL: p.AuthURL, TokenURL: p.TokenURL} return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL}
} }
// UserInfo represents the OpenID Connect userinfo claims. // UserInfo represents the OpenID Connect userinfo claims.
@ -107,11 +158,10 @@ func (u *UserInfo) Claims(v interface{}) error {
// UserInfo uses the token source to query the provider's user info endpoint. // 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) { func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
if p.UserInfoURL == "" { if p.userInfoURL == "" {
return nil, ErrNotSupported return nil, errors.New("oidc: user info endpoint is not supported by this provider")
} }
cli := oauth2.NewClient(ctx, tokenSource) resp, err := clientFromContext(ctx).Get(p.userInfoURL)
resp, err := cli.Get(p.UserInfoURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -137,19 +187,6 @@ func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource)
// //
// The ID Token only holds fields OpenID Connect requires. To access additional // The ID Token only holds fields OpenID Connect requires. To access additional
// claims returned by the server, use the Claims method. // claims returned by the server, use the Claims method.
//
// 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
// }
//
type IDToken struct { type IDToken struct {
// The URL of the server which issued this token. This will always be the same // The URL of the server which issued this token. This will always be the same
// as the URL used for initial discovery. // as the URL used for initial discovery.
@ -165,10 +202,24 @@ type IDToken struct {
Expiry time.Time Expiry time.Time
Nonce string Nonce string
// Raw payload of the id_token.
claims []byte claims []byte
} }
// Claims unmarshals the raw JSON payload of the ID Token into a provided struct. // 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 { func (i *IDToken) Claims(v interface{}) error {
if i.claims == nil { if i.claims == nil {
return errors.New("oidc: claims not set") return errors.New("oidc: claims not set")
@ -176,6 +227,15 @@ func (i *IDToken) Claims(v interface{}) error {
return json.Unmarshal(i.claims, v) return json.Unmarshal(i.claims, v)
} }
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"`
}
type audience []string type audience []string
func (a *audience) UnmarshalJSON(b []byte) error { func (a *audience) UnmarshalJSON(b []byte) error {
@ -192,6 +252,13 @@ func (a *audience) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (a audience) MarshalJSON() ([]byte, error) {
if len(a) == 1 {
return json.Marshal(a[0])
}
return json.Marshal([]string(a))
}
type jsonTime time.Time type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error { func (j *jsonTime) UnmarshalJSON(b []byte) error {
@ -214,113 +281,6 @@ func (j *jsonTime) UnmarshalJSON(b []byte) error {
return nil return nil
} }
type idToken struct { func (j jsonTime) MarshalJSON() ([]byte, error) {
Issuer string `json:"iss"` return json.Marshal(time.Time(j).Unix())
Subject string `json:"sub"`
Audience audience `json:"aud"`
Expiry jsonTime `json:"exp"`
IssuedAt jsonTime `json:"iat"`
Nonce string `json:"nonce"`
}
// IDTokenVerifier provides verification for ID Tokens.
type IDTokenVerifier struct {
issuer string
keySet *remoteKeySet
options []VerificationOption
}
// Verify parse the raw ID Token, verifies it's been signed by the provider, preforms
// additional verification, and returns the claims.
func (v *IDTokenVerifier) Verify(rawIDToken string) (*IDToken, error) {
payload, err := v.keySet.verifyJWT(rawIDToken)
if err != nil {
return nil, err
}
var token idToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
if v.issuer != token.Issuer {
return nil, fmt.Errorf("oidc: iss field did not match provider issuer")
}
t := &IDToken{
Issuer: token.Issuer,
Subject: token.Subject,
Audience: []string(token.Audience),
Expiry: time.Time(token.Expiry),
IssuedAt: time.Time(token.Expiry),
Nonce: token.Nonce,
claims: payload,
}
for _, option := range v.options {
if err := option.verifyIDToken(t); err != nil {
return nil, err
}
}
return t, nil
}
// NewVerifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
//
// The verifier queries the provider to update keys when a signature cannot be verified by the
// set of keys cached from the previous request.
func (p *Provider) NewVerifier(ctx context.Context, options ...VerificationOption) *IDTokenVerifier {
return &IDTokenVerifier{
issuer: p.Issuer,
keySet: newRemoteKeySet(ctx, p.JWKSURL),
options: options,
}
}
// VerificationOption is an option provided to Provider.NewVerifier.
type VerificationOption interface {
verifyIDToken(token *IDToken) error
}
// VerifyAudience ensures that an ID Token was issued for the specific client.
//
// Note that a verified token may be valid for other clients, as OpenID Connect allows a token to have
// multiple audiences.
func VerifyAudience(clientID string) VerificationOption {
return clientVerifier{clientID}
}
type clientVerifier struct {
clientID string
}
func (c clientVerifier) verifyIDToken(token *IDToken) error {
for _, aud := range token.Audience {
if aud == c.clientID {
return nil
}
}
return errors.New("oidc: id token aud field did not match client_id")
}
// VerifyExpiry ensures that an ID Token has not expired.
func VerifyExpiry() VerificationOption {
return expiryVerifier{time.Now}
}
type expiryVerifier struct {
now func() time.Time
}
func (e expiryVerifier) verifyIDToken(token *IDToken) error {
if e.now().After(token.Expiry) {
return ErrTokenExpired
}
return nil
}
// This method is internal to golang.org/x/oauth2. Just copy it.
func contextClient(ctx context.Context) *http.Client {
if ctx != nil {
if hc, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
return hc
}
}
return http.DefaultClient
} }

846
vendor/github.com/coreos/go-oidc/oidc/client.go generated vendored Normal file
View file

@ -0,0 +1,846 @@
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
}

View file

@ -0,0 +1,81 @@
// This file contains tests which depend on the race detector being enabled.
// +build race
package oidc
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
)
type testProvider struct {
baseURL *url.URL
}
func (p *testProvider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != discoveryConfigPath {
http.NotFound(w, r)
return
}
cfg := ProviderConfig{
Issuer: p.baseURL,
ExpiresAt: time.Now().Add(time.Second),
}
cfg = fillRequiredProviderFields(cfg)
json.NewEncoder(w).Encode(&cfg)
}
// This test fails by triggering the race detector, not by calling t.Error or t.Fatal.
func TestProviderSyncRace(t *testing.T) {
prov := &testProvider{}
s := httptest.NewServer(prov)
defer s.Close()
u, err := url.Parse(s.URL)
if err != nil {
t.Fatal(err)
}
prov.baseURL = u
prevValue := minimumProviderConfigSyncInterval
defer func() { minimumProviderConfigSyncInterval = prevValue }()
// Reduce the sync interval to increase the write frequencey.
minimumProviderConfigSyncInterval = 5 * time.Millisecond
cliCfg := ClientConfig{
HTTPClient: http.DefaultClient,
}
cli, err := NewClient(cliCfg)
if err != nil {
t.Error(err)
return
}
if !cli.providerConfig.Get().Empty() {
t.Errorf("want c.ProviderConfig == nil, got c.ProviderConfig=%#v")
}
// SyncProviderConfig beings a goroutine which writes to the client's provider config.
c := cli.SyncProviderConfig(s.URL)
if cli.providerConfig.Get().Empty() {
t.Errorf("want c.ProviderConfig != nil")
}
defer func() {
// stop the background process
c <- struct{}{}
}()
for i := 0; i < 10; i++ {
time.Sleep(5 * time.Millisecond)
// Creating an OAuth client reads from the provider config.
cli.OAuthClient()
}
}

654
vendor/github.com/coreos/go-oidc/oidc/client_test.go generated vendored Normal file
View file

@ -0,0 +1,654 @@
package oidc
import (
"encoding/json"
"net/mail"
"net/url"
"reflect"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oauth2"
"github.com/kylelemons/godebug/pretty"
)
func TestNewClientScopeDefault(t *testing.T) {
tests := []struct {
c ClientConfig
e []string
}{
{
// No scope
c: ClientConfig{RedirectURL: "http://example.com/redirect"},
e: DefaultScope,
},
{
// Nil scope
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: nil},
e: DefaultScope,
},
{
// Empty scope
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{}},
e: []string{},
},
{
// Custom scope equal to default
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{"openid", "email", "profile"}},
e: DefaultScope,
},
{
// Custom scope not including defaults
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{"foo", "bar"}},
e: []string{"foo", "bar"},
},
{
// Custom scopes overlapping with defaults
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{"openid", "foo"}},
e: []string{"openid", "foo"},
},
}
for i, tt := range tests {
c, err := NewClient(tt.c)
if err != nil {
t.Errorf("case %d: unexpected error from NewClient: %v", i, err)
continue
}
if !reflect.DeepEqual(tt.e, c.scope) {
t.Errorf("case %d: want: %v, got: %v", i, tt.e, c.scope)
}
}
}
func TestHealthy(t *testing.T) {
now := time.Now().UTC()
tests := []struct {
p ProviderConfig
h bool
}{
// all ok
{
p: ProviderConfig{
Issuer: &url.URL{Scheme: "http", Host: "example.com"},
ExpiresAt: now.Add(time.Hour),
},
h: true,
},
// zero-value ProviderConfig.ExpiresAt
{
p: ProviderConfig{
Issuer: &url.URL{Scheme: "http", Host: "example.com"},
},
h: true,
},
// expired ProviderConfig
{
p: ProviderConfig{
Issuer: &url.URL{Scheme: "http", Host: "example.com"},
ExpiresAt: now.Add(time.Hour * -1),
},
h: false,
},
// empty ProviderConfig
{
p: ProviderConfig{},
h: false,
},
}
for i, tt := range tests {
c := &Client{providerConfig: newProviderConfigRepo(tt.p)}
err := c.Healthy()
want := tt.h
got := (err == nil)
if want != got {
t.Errorf("case %d: want: healthy=%v, got: healhty=%v, err: %v", i, want, got, err)
}
}
}
func TestClientKeysFuncAll(t *testing.T) {
priv1, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key, error=%v", err)
}
priv2, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key, error=%v", err)
}
now := time.Now()
future := now.Add(time.Hour)
past := now.Add(-1 * time.Hour)
tests := []struct {
keySet *key.PublicKeySet
want []key.PublicKey
}{
// two keys, non-expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, future),
want: []key.PublicKey{*key.NewPublicKey(priv2.JWK()), *key.NewPublicKey(priv1.JWK())},
},
// no keys, non-expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{}, future),
want: []key.PublicKey{},
},
// two keys, expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, past),
want: []key.PublicKey{},
},
// no keys, expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{}, past),
want: []key.PublicKey{},
},
}
for i, tt := range tests {
var c Client
c.keySet = *tt.keySet
keysFunc := c.keysFuncAll()
got := keysFunc()
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, got)
}
}
}
func TestClientKeysFuncWithID(t *testing.T) {
priv1, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key, error=%v", err)
}
priv2, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key, error=%v", err)
}
now := time.Now()
future := now.Add(time.Hour)
past := now.Add(-1 * time.Hour)
tests := []struct {
keySet *key.PublicKeySet
argID string
want []key.PublicKey
}{
// two keys, match, non-expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, future),
argID: priv2.ID(),
want: []key.PublicKey{*key.NewPublicKey(priv2.JWK())},
},
// two keys, no match, non-expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, future),
argID: "XXX",
want: []key.PublicKey{},
},
// no keys, no match, non-expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{}, future),
argID: priv2.ID(),
want: []key.PublicKey{},
},
// two keys, match, expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, past),
argID: priv2.ID(),
want: []key.PublicKey{},
},
// no keys, no match, expired set
{
keySet: key.NewPublicKeySet([]jose.JWK{}, past),
argID: priv2.ID(),
want: []key.PublicKey{},
},
}
for i, tt := range tests {
var c Client
c.keySet = *tt.keySet
keysFunc := c.keysFuncWithID(tt.argID)
got := keysFunc()
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, got)
}
}
}
func TestClientMetadataValid(t *testing.T) {
tests := []ClientMetadata{
// one RedirectURL
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "http", Host: "example.com"}},
},
// one RedirectURL w/ nonempty path
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "http", Host: "example.com", Path: "/foo"}},
},
// two RedirectURIs
ClientMetadata{
RedirectURIs: []url.URL{
url.URL{Scheme: "http", Host: "foo.example.com"},
url.URL{Scheme: "http", Host: "bar.example.com"},
},
},
}
for i, tt := range tests {
if err := tt.Valid(); err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
}
}
}
func TestClientMetadataInvalid(t *testing.T) {
tests := []ClientMetadata{
// nil RedirectURls slice
ClientMetadata{
RedirectURIs: nil,
},
// empty RedirectURIs slice
ClientMetadata{
RedirectURIs: []url.URL{},
},
// empty url.URL
ClientMetadata{
RedirectURIs: []url.URL{url.URL{}},
},
// empty url.URL following OK item
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "http", Host: "example.com"}, url.URL{}},
},
// url.URL with empty Host
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "http", Host: ""}},
},
// url.URL with empty Scheme
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "", Host: "example.com"}},
},
// url.URL with non-HTTP(S) Scheme
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "tcp", Host: "127.0.0.1"}},
},
// EncryptionEnc without EncryptionAlg
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "http", Host: "example.com"}},
IDTokenResponseOptions: JWAOptions{
EncryptionEnc: "A128CBC-HS256",
},
},
// List of URIs with one empty element
ClientMetadata{
RedirectURIs: []url.URL{url.URL{Scheme: "http", Host: "example.com"}},
RequestURIs: []url.URL{
url.URL{Scheme: "http", Host: "example.com"},
url.URL{},
},
},
}
for i, tt := range tests {
if err := tt.Valid(); err == nil {
t.Errorf("case %d: expected non-nil error", i)
}
}
}
func TestChooseAuthMethod(t *testing.T) {
tests := []struct {
supported []string
chosen string
err bool
}{
{
supported: []string{},
chosen: oauth2.AuthMethodClientSecretBasic,
},
{
supported: []string{oauth2.AuthMethodClientSecretBasic},
chosen: oauth2.AuthMethodClientSecretBasic,
},
{
supported: []string{oauth2.AuthMethodClientSecretPost},
chosen: oauth2.AuthMethodClientSecretPost,
},
{
supported: []string{oauth2.AuthMethodClientSecretPost, oauth2.AuthMethodClientSecretBasic},
chosen: oauth2.AuthMethodClientSecretPost,
},
{
supported: []string{oauth2.AuthMethodClientSecretBasic, oauth2.AuthMethodClientSecretPost},
chosen: oauth2.AuthMethodClientSecretBasic,
},
{
supported: []string{oauth2.AuthMethodClientSecretJWT, oauth2.AuthMethodClientSecretPost},
chosen: oauth2.AuthMethodClientSecretPost,
},
{
supported: []string{oauth2.AuthMethodClientSecretJWT},
chosen: "",
err: true,
},
}
for i, tt := range tests {
cfg := ProviderConfig{
TokenEndpointAuthMethodsSupported: tt.supported,
}
got, err := chooseAuthMethod(cfg)
if tt.err {
if err == nil {
t.Errorf("case %d: expected non-nil err", i)
}
continue
}
if got != tt.chosen {
t.Errorf("case %d: want=%q, got=%q", i, tt.chosen, got)
}
}
}
func TestClientMetadataUnmarshal(t *testing.T) {
tests := []struct {
data string
want ClientMetadata
wantErr bool
}{
{
`{"redirect_uris":["https://example.com"]}`,
ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com"},
},
},
false,
},
{
// redirect_uris required
`{}`,
ClientMetadata{},
true,
},
{
// must have at least one redirect_uris
`{"redirect_uris":[]}`,
ClientMetadata{},
true,
},
{
`{"redirect_uris":["https://example.com"],"contacts":["Ms. Foo <foo@example.com>"]}`,
ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com"},
},
Contacts: []mail.Address{
{Name: "Ms. Foo", Address: "foo@example.com"},
},
},
false,
},
{
// invalid URI provided for field
`{"redirect_uris":["https://example.com"],"logo_uri":"not a valid uri"}`,
ClientMetadata{},
true,
},
{
// logo_uri can't be a list
`{"redirect_uris":["https://example.com"],"logo_uri":["https://example.com/logo"]}`,
ClientMetadata{},
true,
},
{
`{
"redirect_uris":["https://example.com"],
"userinfo_encrypted_response_alg":"RSA1_5",
"userinfo_encrypted_response_enc":"A128CBC-HS256",
"contacts": [
"jane doe <jane.doe@example.com>", "john doe <john.doe@example.com>"
]
}`,
ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com"},
},
UserInfoResponseOptions: JWAOptions{
EncryptionAlg: "RSA1_5",
EncryptionEnc: "A128CBC-HS256",
},
Contacts: []mail.Address{
{Name: "jane doe", Address: "jane.doe@example.com"},
{Name: "john doe", Address: "john.doe@example.com"},
},
},
false,
},
{
// If encrypted_response_enc is provided encrypted_response_alg must also be.
`{
"redirect_uris":["https://example.com"],
"userinfo_encrypted_response_enc":"A128CBC-HS256"
}`,
ClientMetadata{},
true,
},
}
for i, tt := range tests {
var got ClientMetadata
if err := got.UnmarshalJSON([]byte(tt.data)); err != nil {
if !tt.wantErr {
t.Errorf("case %d: unmarshal failed: %v", i, err)
}
continue
}
if tt.wantErr {
t.Errorf("case %d: expected unmarshal to produce error", i)
continue
}
if diff := pretty.Compare(tt.want, got); diff != "" {
t.Errorf("case %d: results not equal: %s", i, diff)
}
}
}
func TestClientMetadataMarshal(t *testing.T) {
tests := []struct {
metadata ClientMetadata
want string
}{
{
ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com", Path: "/callback"},
},
},
`{"redirect_uris":["https://example.com/callback"]}`,
},
{
ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com", Path: "/callback"},
},
RequestObjectOptions: JWAOptions{
EncryptionAlg: "RSA1_5",
EncryptionEnc: "A128CBC-HS256",
},
},
`{"redirect_uris":["https://example.com/callback"],"request_object_encryption_alg":"RSA1_5","request_object_encryption_enc":"A128CBC-HS256"}`,
},
}
for i, tt := range tests {
got, err := json.Marshal(&tt.metadata)
if err != nil {
t.Errorf("case %d: failed to marshal metadata: %v", i, err)
continue
}
if string(got) != tt.want {
t.Errorf("case %d: marshaled string did not match expected string", i)
}
}
}
func TestClientMetadataMarshalRoundTrip(t *testing.T) {
tests := []ClientMetadata{
{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com", Path: "/callback"},
},
LogoURI: &url.URL{Scheme: "https", Host: "example.com", Path: "/logo"},
RequestObjectOptions: JWAOptions{
EncryptionAlg: "RSA1_5",
EncryptionEnc: "A128CBC-HS256",
},
ApplicationType: "native",
TokenEndpointAuthMethod: "client_secret_basic",
},
}
for i, want := range tests {
data, err := json.Marshal(&want)
if err != nil {
t.Errorf("case %d: failed to marshal metadata: %v", i, err)
continue
}
var got ClientMetadata
if err := json.Unmarshal(data, &got); err != nil {
t.Errorf("case %d: failed to unmarshal metadata: %v", i, err)
continue
}
if diff := pretty.Compare(want, got); diff != "" {
t.Errorf("case %d: struct did not survive a marshaling round trip: %s", i, diff)
}
}
}
func TestClientRegistrationResponseUnmarshal(t *testing.T) {
tests := []struct {
data string
want ClientRegistrationResponse
wantErr bool
secretExpires bool
}{
{
`{
"client_id":"foo",
"client_secret":"bar",
"client_secret_expires_at": 1577858400,
"redirect_uris":[
"https://client.example.org/callback",
"https://client.example.org/callback2"
],
"client_name":"my_example"
}`,
ClientRegistrationResponse{
ClientID: "foo",
ClientSecret: "bar",
ClientSecretExpiresAt: time.Unix(1577858400, 0),
ClientMetadata: ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "client.example.org", Path: "/callback"},
{Scheme: "https", Host: "client.example.org", Path: "/callback2"},
},
ClientName: "my_example",
},
},
false,
true,
},
{
`{
"client_id":"foo",
"client_secret_expires_at": 0,
"redirect_uris":[
"https://client.example.org/callback",
"https://client.example.org/callback2"
],
"client_name":"my_example"
}`,
ClientRegistrationResponse{
ClientID: "foo",
ClientMetadata: ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "client.example.org", Path: "/callback"},
{Scheme: "https", Host: "client.example.org", Path: "/callback2"},
},
ClientName: "my_example",
},
},
false,
false,
},
{
// no client id
`{
"client_secret_expires_at": 0,
"redirect_uris":[
"https://client.example.org/callback",
"https://client.example.org/callback2"
],
"client_name":"my_example"
}`,
ClientRegistrationResponse{},
true,
false,
},
}
for i, tt := range tests {
var got ClientRegistrationResponse
if err := json.Unmarshal([]byte(tt.data), &got); err != nil {
if !tt.wantErr {
t.Errorf("case %d: unmarshal failed: %v", i, err)
}
continue
}
if tt.wantErr {
t.Errorf("case %d: expected unmarshal to produce error", i)
continue
}
if diff := pretty.Compare(tt.want, got); diff != "" {
t.Errorf("case %d: results not equal: %s", i, diff)
}
if tt.secretExpires && got.ClientSecretExpiresAt.IsZero() {
t.Errorf("case %d: expected client_secret to expire, but it doesn't", i)
} else if !tt.secretExpires && !got.ClientSecretExpiresAt.IsZero() {
t.Errorf("case %d: expected client_secret to not expire, but it does", i)
}
}
}

2
vendor/github.com/coreos/go-oidc/oidc/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package oidc is DEPRECATED. Use github.com/coreos/go-oidc instead.
package oidc

44
vendor/github.com/coreos/go-oidc/oidc/identity.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
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
}

113
vendor/github.com/coreos/go-oidc/oidc/identity_test.go generated vendored Normal file
View file

@ -0,0 +1,113 @@
package oidc
import (
"reflect"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
)
func TestIdentityFromClaims(t *testing.T) {
tests := []struct {
claims jose.Claims
want Identity
}{
{
claims: jose.Claims{
"sub": "123850281",
"name": "Elroy",
"email": "elroy@example.com",
"exp": float64(1.416935146e+09),
},
want: Identity{
ID: "123850281",
Name: "",
Email: "elroy@example.com",
ExpiresAt: time.Date(2014, time.November, 25, 17, 05, 46, 0, time.UTC),
},
},
{
claims: jose.Claims{
"sub": "123850281",
"name": "Elroy",
"exp": float64(1.416935146e+09),
},
want: Identity{
ID: "123850281",
Name: "",
Email: "",
ExpiresAt: time.Date(2014, time.November, 25, 17, 05, 46, 0, time.UTC),
},
},
{
claims: jose.Claims{
"sub": "123850281",
"name": "Elroy",
"email": "elroy@example.com",
"exp": int64(1416935146),
},
want: Identity{
ID: "123850281",
Name: "",
Email: "elroy@example.com",
ExpiresAt: time.Date(2014, time.November, 25, 17, 05, 46, 0, time.UTC),
},
},
{
claims: jose.Claims{
"sub": "123850281",
"name": "Elroy",
"email": "elroy@example.com",
},
want: Identity{
ID: "123850281",
Name: "",
Email: "elroy@example.com",
ExpiresAt: time.Time{},
},
},
}
for i, tt := range tests {
got, err := IdentityFromClaims(tt.claims)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(tt.want, *got) {
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, *got)
}
}
}
func TestIdentityFromClaimsFail(t *testing.T) {
tests := []jose.Claims{
// sub incorrect type
jose.Claims{
"sub": 123,
"name": "foo",
"email": "elroy@example.com",
},
// email incorrect type
jose.Claims{
"sub": "123850281",
"name": "Elroy",
"email": false,
},
// exp incorrect type
jose.Claims{
"sub": "123850281",
"name": "Elroy",
"email": "elroy@example.com",
"exp": "2014-11-25 18:05:46 +0000 UTC",
},
}
for i, tt := range tests {
_, err := IdentityFromClaims(tt)
if err == nil {
t.Errorf("case %d: expected non-nil error", i)
}
}
}

3
vendor/github.com/coreos/go-oidc/oidc/interface.go generated vendored Normal file
View file

@ -0,0 +1,3 @@
package oidc
type LoginFunc func(ident Identity, sessionKey string) (redirectURL string, err error)

67
vendor/github.com/coreos/go-oidc/oidc/key.go generated vendored Executable file
View file

@ -0,0 +1,67 @@
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
}

690
vendor/github.com/coreos/go-oidc/oidc/provider.go generated vendored Normal file
View file

@ -0,0 +1,690 @@
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'")
}
if contains(p.TokenEndpointAuthMethodsSupported, "none") {
return errors.New("token_endpoint_auth_signing_alg_values_supported cannot include 'none'")
}
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 falied, retyring 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 falied, retyring 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
}

940
vendor/github.com/coreos/go-oidc/oidc/provider_test.go generated vendored Normal file
View file

@ -0,0 +1,940 @@
package oidc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"time"
"github.com/jonboulle/clockwork"
"github.com/kylelemons/godebug/diff"
"github.com/kylelemons/godebug/pretty"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oauth2"
)
func TestProviderConfigDefaults(t *testing.T) {
var cfg ProviderConfig
cfg = cfg.Defaults()
tests := []struct {
got, want []string
name string
}{
{cfg.GrantTypesSupported, DefaultGrantTypesSupported, "grant types"},
{cfg.ResponseModesSupported, DefaultResponseModesSupported, "response modes"},
{cfg.ClaimTypesSupported, DefaultClaimTypesSupported, "claim types"},
{
cfg.TokenEndpointAuthMethodsSupported,
DefaultTokenEndpointAuthMethodsSupported,
"token endpoint auth methods",
},
}
for _, tt := range tests {
if diff := pretty.Compare(tt.want, tt.got); diff != "" {
t.Errorf("%s: did not match %s", tt.name, diff)
}
}
}
func TestProviderConfigUnmarshal(t *testing.T) {
// helper for quickly creating uris
uri := func(path string) *url.URL {
return &url.URL{
Scheme: "https",
Host: "server.example.com",
Path: path,
}
}
tests := []struct {
data string
want ProviderConfig
wantErr bool
}{
{
data: `{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/connect/authorize",
"token_endpoint": "https://server.example.com/connect/token",
"token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"],
"token_endpoint_auth_signing_alg_values_supported": ["RS256", "ES256"],
"userinfo_endpoint": "https://server.example.com/connect/userinfo",
"jwks_uri": "https://server.example.com/jwks.json",
"registration_endpoint": "https://server.example.com/connect/register",
"scopes_supported": [
"openid", "profile", "email", "address", "phone", "offline_access"
],
"response_types_supported": [
"code", "code id_token", "id_token", "id_token token"
],
"acr_values_supported": [
"urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"
],
"subject_types_supported": ["public", "pairwise"],
"userinfo_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
"userinfo_encryption_alg_values_supported": ["RSA1_5", "A128KW"],
"userinfo_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"],
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
"id_token_encryption_alg_values_supported": ["RSA1_5", "A128KW"],
"id_token_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"],
"request_object_signing_alg_values_supported": ["none", "RS256", "ES256"],
"display_values_supported": ["page", "popup"],
"claim_types_supported": ["normal", "distributed"],
"claims_supported": [
"sub", "iss", "auth_time", "acr", "name", "given_name",
"family_name", "nickname", "profile", "picture", "website",
"email", "email_verified", "locale", "zoneinfo",
"http://example.info/claims/groups"
],
"claims_parameter_supported": true,
"service_documentation": "https://server.example.com/connect/service_documentation.html",
"ui_locales_supported": ["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
}
`,
want: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "server.example.com"},
AuthEndpoint: uri("/connect/authorize"),
TokenEndpoint: uri("/connect/token"),
TokenEndpointAuthMethodsSupported: []string{
oauth2.AuthMethodClientSecretBasic, oauth2.AuthMethodPrivateKeyJWT,
},
TokenEndpointAuthSigningAlgValuesSupported: []string{
jose.AlgRS256, jose.AlgES256,
},
UserInfoEndpoint: uri("/connect/userinfo"),
KeysEndpoint: uri("/jwks.json"),
RegistrationEndpoint: uri("/connect/register"),
ScopesSupported: []string{
"openid", "profile", "email", "address", "phone", "offline_access",
},
ResponseTypesSupported: []string{
oauth2.ResponseTypeCode, oauth2.ResponseTypeCodeIDToken,
oauth2.ResponseTypeIDToken, oauth2.ResponseTypeIDTokenToken,
},
ACRValuesSupported: []string{
"urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze",
},
SubjectTypesSupported: []string{
SubjectTypePublic, SubjectTypePairwise,
},
UserInfoSigningAlgValues: []string{jose.AlgRS256, jose.AlgES256, jose.AlgHS256},
UserInfoEncryptionAlgValues: []string{"RSA1_5", "A128KW"},
UserInfoEncryptionEncValues: []string{"A128CBC-HS256", "A128GCM"},
IDTokenSigningAlgValues: []string{jose.AlgRS256, jose.AlgES256, jose.AlgHS256},
IDTokenEncryptionAlgValues: []string{"RSA1_5", "A128KW"},
IDTokenEncryptionEncValues: []string{"A128CBC-HS256", "A128GCM"},
ReqObjSigningAlgValues: []string{jose.AlgNone, jose.AlgRS256, jose.AlgES256},
DisplayValuesSupported: []string{"page", "popup"},
ClaimTypesSupported: []string{"normal", "distributed"},
ClaimsSupported: []string{
"sub", "iss", "auth_time", "acr", "name", "given_name",
"family_name", "nickname", "profile", "picture", "website",
"email", "email_verified", "locale", "zoneinfo",
"http://example.info/claims/groups",
},
ClaimsParameterSupported: true,
ServiceDocs: uri("/connect/service_documentation.html"),
UILocalsSupported: []string{"en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"},
},
wantErr: false,
},
{
// missing a lot of required field
data: `{}`,
wantErr: true,
},
{
data: `{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/connect/authorize",
"token_endpoint": "https://server.example.com/connect/token",
"jwks_uri": "https://server.example.com/jwks.json",
"response_types_supported": [
"code", "code id_token", "id_token", "id_token token"
],
"subject_types_supported": ["public", "pairwise"],
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"]
}
`,
want: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "server.example.com"},
AuthEndpoint: uri("/connect/authorize"),
TokenEndpoint: uri("/connect/token"),
KeysEndpoint: uri("/jwks.json"),
ResponseTypesSupported: []string{
oauth2.ResponseTypeCode, oauth2.ResponseTypeCodeIDToken,
oauth2.ResponseTypeIDToken, oauth2.ResponseTypeIDTokenToken,
},
SubjectTypesSupported: []string{
SubjectTypePublic, SubjectTypePairwise,
},
IDTokenSigningAlgValues: []string{jose.AlgRS256, jose.AlgES256, jose.AlgHS256},
},
wantErr: false,
},
{
// invalid scheme 'ftp://'
data: `{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/connect/authorize",
"token_endpoint": "https://server.example.com/connect/token",
"jwks_uri": "ftp://server.example.com/jwks.json",
"response_types_supported": [
"code", "code id_token", "id_token", "id_token token"
],
"subject_types_supported": ["public", "pairwise"],
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"]
}
`,
wantErr: true,
},
}
for i, tt := range tests {
var got ProviderConfig
if err := json.Unmarshal([]byte(tt.data), &got); err != nil {
if !tt.wantErr {
t.Errorf("case %d: failed to unmarshal provider config: %v", i, err)
}
continue
}
if tt.wantErr {
t.Errorf("case %d: expected error", i)
continue
}
if diff := pretty.Compare(tt.want, got); diff != "" {
t.Errorf("case %d: unmarshaled struct did not match expected %s", i, diff)
}
}
}
func TestProviderConfigMarshal(t *testing.T) {
tests := []struct {
cfg ProviderConfig
want string
}{
{
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "auth.example.com"},
AuthEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/auth",
},
TokenEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/token",
},
UserInfoEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/userinfo",
},
KeysEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/jwk",
},
ResponseTypesSupported: []string{oauth2.ResponseTypeCode},
SubjectTypesSupported: []string{SubjectTypePublic},
IDTokenSigningAlgValues: []string{jose.AlgRS256},
},
// spacing must match json.MarshalIndent(cfg, "", "\t")
want: `{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/auth",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/jwk",
"response_types_supported": [
"code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
]
}`,
},
{
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "auth.example.com"},
AuthEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/auth",
},
TokenEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/token",
},
UserInfoEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/userinfo",
},
KeysEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/jwk",
},
RegistrationEndpoint: &url.URL{
Scheme: "https", Host: "auth.example.com", Path: "/register",
},
ScopesSupported: DefaultScope,
ResponseTypesSupported: []string{oauth2.ResponseTypeCode},
ResponseModesSupported: DefaultResponseModesSupported,
GrantTypesSupported: []string{oauth2.GrantTypeAuthCode},
SubjectTypesSupported: []string{SubjectTypePublic},
IDTokenSigningAlgValues: []string{jose.AlgRS256},
ServiceDocs: &url.URL{Scheme: "https", Host: "example.com", Path: "/docs"},
},
// spacing must match json.MarshalIndent(cfg, "", "\t")
want: `{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/auth",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/jwk",
"registration_endpoint": "https://auth.example.com/register",
"scopes_supported": [
"openid",
"email",
"profile"
],
"response_types_supported": [
"code"
],
"response_modes_supported": [
"query",
"fragment"
],
"grant_types_supported": [
"authorization_code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"service_documentation": "https://example.com/docs"
}`,
},
}
for i, tt := range tests {
got, err := json.MarshalIndent(&tt.cfg, "", "\t")
if err != nil {
t.Errorf("case %d: failed to marshal config: %v", i, err)
continue
}
if d := diff.Diff(string(got), string(tt.want)); d != "" {
t.Errorf("case %d: expected did not match result: %s", i, d)
}
var cfg ProviderConfig
if err := json.Unmarshal(got, &cfg); err != nil {
t.Errorf("case %d: could not unmarshal marshal response: %v", i, err)
continue
}
if d := pretty.Compare(tt.cfg, cfg); d != "" {
t.Errorf("case %d: config did not survive JSON marshaling round trip: %s", i, d)
}
}
}
func TestProviderConfigSupports(t *testing.T) {
tests := []struct {
provider ProviderConfig
client ClientMetadata
fillRequiredProviderFields bool
ok bool
}{
{
provider: ProviderConfig{},
client: ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com", Path: "/callback"},
},
},
fillRequiredProviderFields: true,
ok: true,
},
{
// invalid provider config
provider: ProviderConfig{},
client: ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "example.com", Path: "/callback"},
},
},
fillRequiredProviderFields: false,
ok: false,
},
{
// invalid client config
provider: ProviderConfig{},
client: ClientMetadata{},
fillRequiredProviderFields: true,
ok: false,
},
}
for i, tt := range tests {
if tt.fillRequiredProviderFields {
tt.provider = fillRequiredProviderFields(tt.provider)
}
err := tt.provider.Supports(tt.client)
if err == nil && !tt.ok {
t.Errorf("case %d: expected non-nil error", i)
}
if err != nil && tt.ok {
t.Errorf("case %d: supports failed: %v", i, err)
}
}
}
func newValidProviderConfig() ProviderConfig {
var cfg ProviderConfig
return fillRequiredProviderFields(cfg)
}
// fill a provider config with enough information to be valid
func fillRequiredProviderFields(cfg ProviderConfig) ProviderConfig {
if cfg.Issuer == nil {
cfg.Issuer = &url.URL{Scheme: "https", Host: "auth.example.com"}
}
urlPath := func(path string) *url.URL {
var u url.URL
u = *cfg.Issuer
u.Path = path
return &u
}
cfg.AuthEndpoint = urlPath("/auth")
cfg.TokenEndpoint = urlPath("/token")
cfg.UserInfoEndpoint = urlPath("/userinfo")
cfg.KeysEndpoint = urlPath("/jwk")
cfg.ResponseTypesSupported = []string{oauth2.ResponseTypeCode}
cfg.SubjectTypesSupported = []string{SubjectTypePublic}
cfg.IDTokenSigningAlgValues = []string{jose.AlgRS256}
return cfg
}
type fakeProviderConfigGetterSetter struct {
cfg *ProviderConfig
getCount int
setCount int
}
func (g *fakeProviderConfigGetterSetter) Get() (ProviderConfig, error) {
g.getCount++
return *g.cfg, nil
}
func (g *fakeProviderConfigGetterSetter) Set(cfg ProviderConfig) error {
g.cfg = &cfg
g.setCount++
return nil
}
type fakeProviderConfigHandler struct {
cfg ProviderConfig
maxAge time.Duration
}
func (s *fakeProviderConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(&s.cfg)
if s.maxAge.Seconds() >= 0 {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(s.maxAge.Seconds())))
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
func TestProviderConfigRequiredFields(t *testing.T) {
// Ensure provider metadata responses have all the required fields.
// taken from https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
requiredFields := []string{
"issuer",
"authorization_endpoint",
"token_endpoint", // "This is REQUIRED unless only the Implicit Flow is used."
"jwks_uri",
"response_types_supported",
"subject_types_supported",
"id_token_signing_alg_values_supported",
}
svr := &fakeProviderConfigHandler{
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "http", Host: "example.com"},
ExpiresAt: time.Now().Add(time.Minute),
},
maxAge: time.Minute,
}
svr.cfg = fillRequiredProviderFields(svr.cfg)
s := httptest.NewServer(svr)
defer s.Close()
resp, err := http.Get(s.URL + "/")
if err != nil {
t.Errorf("get: %v", err)
return
}
defer resp.Body.Close()
var data map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
t.Errorf("decode: %v", err)
return
}
for _, field := range requiredFields {
if _, ok := data[field]; !ok {
t.Errorf("provider metadata does not have required field '%s'", field)
}
}
}
type handlerClient struct {
Handler http.Handler
}
func (hc *handlerClient) Do(r *http.Request) (*http.Response, error) {
w := httptest.NewRecorder()
hc.Handler.ServeHTTP(w, r)
resp := http.Response{
StatusCode: w.Code,
Header: w.Header(),
Body: ioutil.NopCloser(w.Body),
}
return &resp, nil
}
func TestHTTPProviderConfigGetter(t *testing.T) {
svr := &fakeProviderConfigHandler{}
hc := &handlerClient{Handler: svr}
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
tests := []struct {
dsc string
age time.Duration
cfg ProviderConfig
ok bool
}{
// everything is good
{
dsc: "https://example.com",
age: time.Minute,
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
ExpiresAt: now.Add(time.Minute),
},
ok: true,
},
// iss and disco url differ by scheme only (how google works)
{
dsc: "https://example.com",
age: time.Minute,
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
ExpiresAt: now.Add(time.Minute),
},
ok: true,
},
// issuer and discovery URL mismatch
{
dsc: "https://foo.com",
age: time.Minute,
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
ExpiresAt: now.Add(time.Minute),
},
ok: false,
},
// missing cache header results in zero ExpiresAt
{
dsc: "https://example.com",
age: -1,
cfg: ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
},
ok: true,
},
}
for i, tt := range tests {
tt.cfg = fillRequiredProviderFields(tt.cfg)
svr.cfg = tt.cfg
svr.maxAge = tt.age
getter := NewHTTPProviderConfigGetter(hc, tt.dsc)
getter.clock = fc
got, err := getter.Get()
if err != nil {
if tt.ok {
t.Errorf("test %d: unexpected error: %v", i, err)
}
continue
}
if !tt.ok {
t.Errorf("test %d: expected error", i)
continue
}
if !reflect.DeepEqual(tt.cfg, got) {
t.Errorf("test %d: want: %#v, got: %#v", i, tt.cfg, got)
}
}
}
func TestProviderConfigSyncerRun(t *testing.T) {
c1 := &ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
}
c2 := &ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
}
tests := []struct {
first *ProviderConfig
advance time.Duration
second *ProviderConfig
firstExp time.Duration
secondExp time.Duration
count int
}{
// exp is 10m, should have same config after 1s
{
first: c1,
firstExp: time.Duration(10 * time.Minute),
advance: time.Minute,
second: c1,
secondExp: time.Duration(10 * time.Minute),
count: 1,
},
// exp is 10m, should have new config after 10/2 = 5m
{
first: c1,
firstExp: time.Duration(10 * time.Minute),
advance: time.Duration(5 * time.Minute),
second: c2,
secondExp: time.Duration(10 * time.Minute),
count: 2,
},
// exp is 20m, should have new config after 20/2 = 10m
{
first: c1,
firstExp: time.Duration(20 * time.Minute),
advance: time.Duration(10 * time.Minute),
second: c2,
secondExp: time.Duration(30 * time.Minute),
count: 2,
},
}
assertCfg := func(i int, to *fakeProviderConfigGetterSetter, want ProviderConfig) {
got, err := to.Get()
if err != nil {
t.Fatalf("test %d: unable to get config: %v", i, err)
}
if !reflect.DeepEqual(want, got) {
t.Fatalf("test %d: incorrect state:\nwant=%#v\ngot=%#v", i, want, got)
}
}
for i, tt := range tests {
from := &fakeProviderConfigGetterSetter{}
to := &fakeProviderConfigGetterSetter{}
fc := clockwork.NewFakeClock()
now := fc.Now().UTC()
syncer := NewProviderConfigSyncer(from, to)
syncer.clock = fc
tt.first.ExpiresAt = now.Add(tt.firstExp)
tt.second.ExpiresAt = now.Add(tt.secondExp)
if err := from.Set(*tt.first); err != nil {
t.Fatalf("test %d: unexpected error: %v", i, err)
}
stop := syncer.Run()
defer close(stop)
fc.BlockUntil(1)
// first sync
assertCfg(i, to, *tt.first)
if err := from.Set(*tt.second); err != nil {
t.Fatalf("test %d: unexpected error: %v", i, err)
}
fc.Advance(tt.advance)
fc.BlockUntil(1)
// second sync
assertCfg(i, to, *tt.second)
if tt.count != from.getCount {
t.Fatalf("test %d: want: %v, got: %v", i, tt.count, from.getCount)
}
}
}
type staticProviderConfigGetter struct {
cfg ProviderConfig
err error
}
func (g *staticProviderConfigGetter) Get() (ProviderConfig, error) {
return g.cfg, g.err
}
type staticProviderConfigSetter struct {
cfg *ProviderConfig
err error
}
func (s *staticProviderConfigSetter) Set(cfg ProviderConfig) error {
s.cfg = &cfg
return s.err
}
func TestProviderConfigSyncerSyncFailure(t *testing.T) {
fc := clockwork.NewFakeClock()
tests := []struct {
from *staticProviderConfigGetter
to *staticProviderConfigSetter
// want indicates what ProviderConfig should be passed to Set.
// If nil, the Set should not be called.
want *ProviderConfig
}{
// generic Get failure
{
from: &staticProviderConfigGetter{err: errors.New("fail")},
to: &staticProviderConfigSetter{},
want: nil,
},
// generic Set failure
{
from: &staticProviderConfigGetter{cfg: ProviderConfig{ExpiresAt: fc.Now().Add(time.Minute)}},
to: &staticProviderConfigSetter{err: errors.New("fail")},
want: &ProviderConfig{ExpiresAt: fc.Now().Add(time.Minute)},
},
}
for i, tt := range tests {
pcs := &ProviderConfigSyncer{
from: tt.from,
to: tt.to,
clock: fc,
}
_, err := pcs.sync()
if err == nil {
t.Errorf("case %d: expected non-nil error", i)
}
if !reflect.DeepEqual(tt.want, tt.to.cfg) {
t.Errorf("case %d: Set mismatch: want=%#v got=%#v", i, tt.want, tt.to.cfg)
}
}
}
func TestNextSyncAfter(t *testing.T) {
fc := clockwork.NewFakeClock()
tests := []struct {
exp time.Time
want time.Duration
}{
{
exp: fc.Now().Add(time.Hour),
want: 30 * time.Minute,
},
// override large values with the maximum
{
exp: fc.Now().Add(168 * time.Hour), // one week
want: 24 * time.Hour,
},
// override "now" values with the minimum
{
exp: fc.Now(),
want: time.Minute,
},
// override negative values with the minimum
{
exp: fc.Now().Add(-1 * time.Minute),
want: time.Minute,
},
// zero-value Time results in maximum sync interval
{
exp: time.Time{},
want: 24 * time.Hour,
},
}
for i, tt := range tests {
got := nextSyncAfter(tt.exp, fc)
if tt.want != got {
t.Errorf("case %d: want=%v got=%v", i, tt.want, got)
}
}
}
func TestProviderConfigEmpty(t *testing.T) {
cfg := ProviderConfig{}
if !cfg.Empty() {
t.Fatalf("Empty provider config reports non-empty")
}
cfg = ProviderConfig{
Issuer: &url.URL{Scheme: "https", Host: "example.com"},
}
if cfg.Empty() {
t.Fatalf("Non-empty provider config reports empty")
}
}
func TestPCSStepAfter(t *testing.T) {
pass := func() (time.Duration, error) { return 7 * time.Second, nil }
fail := func() (time.Duration, error) { return 0, errors.New("fail") }
tests := []struct {
stepper pcsStepper
stepFunc pcsStepFunc
want pcsStepper
}{
// good step results in retry at TTL
{
stepper: &pcsStepNext{},
stepFunc: pass,
want: &pcsStepNext{aft: 7 * time.Second},
},
// good step after failed step results results in retry at TTL
{
stepper: &pcsStepRetry{aft: 2 * time.Second},
stepFunc: pass,
want: &pcsStepNext{aft: 7 * time.Second},
},
// failed step results in a retry in 1s
{
stepper: &pcsStepNext{},
stepFunc: fail,
want: &pcsStepRetry{aft: time.Second},
},
// failed retry backs off by a factor of 2
{
stepper: &pcsStepRetry{aft: time.Second},
stepFunc: fail,
want: &pcsStepRetry{aft: 2 * time.Second},
},
// failed retry backs off by a factor of 2, up to 1m
{
stepper: &pcsStepRetry{aft: 32 * time.Second},
stepFunc: fail,
want: &pcsStepRetry{aft: 60 * time.Second},
},
}
for i, tt := range tests {
got := tt.stepper.step(tt.stepFunc)
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, got)
}
}
}
func TestProviderConfigSupportsGrantType(t *testing.T) {
tests := []struct {
types []string
typ string
want bool
}{
// explicitly supported
{
types: []string{"foo_type"},
typ: "foo_type",
want: true,
},
// explicitly unsupported
{
types: []string{"bar_type"},
typ: "foo_type",
want: false,
},
// default type explicitly unsupported
{
types: []string{oauth2.GrantTypeImplicit},
typ: oauth2.GrantTypeAuthCode,
want: false,
},
// type not found in default set
{
types: []string{},
typ: "foo_type",
want: false,
},
// type found in default set
{
types: []string{},
typ: oauth2.GrantTypeAuthCode,
want: true,
},
}
for i, tt := range tests {
cfg := ProviderConfig{
GrantTypesSupported: tt.types,
}
got := cfg.SupportsGrantType(tt.typ)
if tt.want != got {
t.Errorf("case %d: assert %v supports %v: want=%t got=%t", i, tt.types, tt.typ, tt.want, got)
}
}
}
type fakeClient struct {
resp *http.Response
}
func (f *fakeClient) Do(req *http.Request) (*http.Response, error) {
return f.resp, nil
}
func TestWaitForProviderConfigImmediateSuccess(t *testing.T) {
cfg := newValidProviderConfig()
b, err := json.Marshal(&cfg)
if err != nil {
t.Fatalf("Failed marshaling provider config")
}
resp := http.Response{Body: ioutil.NopCloser(bytes.NewBuffer(b))}
hc := &fakeClient{&resp}
fc := clockwork.NewFakeClock()
reschan := make(chan ProviderConfig)
go func() {
reschan <- waitForProviderConfig(hc, cfg.Issuer.String(), fc)
}()
var got ProviderConfig
select {
case got = <-reschan:
case <-time.After(time.Second):
t.Fatalf("Did not receive result within 1s")
}
if !reflect.DeepEqual(cfg, got) {
t.Fatalf("Received incorrect provider config: want=%#v got=%#v", cfg, got)
}
}

88
vendor/github.com/coreos/go-oidc/oidc/transport.go generated vendored Normal file
View file

@ -0,0 +1,88 @@
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)
}

176
vendor/github.com/coreos/go-oidc/oidc/transport_test.go generated vendored Normal file
View file

@ -0,0 +1,176 @@
package oidc
import (
"errors"
"net/http"
"reflect"
"testing"
"github.com/coreos/go-oidc/jose"
)
type staticTokenRefresher struct {
verify func(jose.JWT) error
refresh func() (jose.JWT, error)
}
func (s *staticTokenRefresher) Verify(jwt jose.JWT) error {
return s.verify(jwt)
}
func (s *staticTokenRefresher) Refresh() (jose.JWT, error) {
return s.refresh()
}
func TestAuthenticatedTransportVerifiedJWT(t *testing.T) {
tests := []struct {
refresher TokenRefresher
startJWT jose.JWT
wantJWT jose.JWT
wantError error
}{
// verification succeeds, so refresh is not called
{
refresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return nil },
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "2"}, nil },
},
startJWT: jose.JWT{RawPayload: "1"},
wantJWT: jose.JWT{RawPayload: "1"},
},
// verification fails, refresh succeeds so cached JWT changes
{
refresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return errors.New("fail!") },
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "2"}, nil },
},
startJWT: jose.JWT{RawPayload: "1"},
wantJWT: jose.JWT{RawPayload: "2"},
},
// verification succeeds, so failing refresh isn't attempted
{
refresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return nil },
refresh: func() (jose.JWT, error) { return jose.JWT{}, errors.New("fail!") },
},
startJWT: jose.JWT{RawPayload: "1"},
wantJWT: jose.JWT{RawPayload: "1"},
},
// verification fails, but refresh fails, too
{
refresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return errors.New("fail!") },
refresh: func() (jose.JWT, error) { return jose.JWT{}, errors.New("fail!") },
},
startJWT: jose.JWT{RawPayload: "1"},
wantJWT: jose.JWT{},
wantError: errors.New("unable to acquire valid JWT: fail!"),
},
}
for i, tt := range tests {
at := &AuthenticatedTransport{
TokenRefresher: tt.refresher,
}
at.SetJWT(tt.startJWT)
gotJWT, err := at.verifiedJWT()
if !reflect.DeepEqual(tt.wantError, err) {
t.Errorf("#%d: unexpected error: want=%#v got=%#v", i, tt.wantError, err)
}
if !reflect.DeepEqual(tt.wantJWT, gotJWT) {
t.Errorf("#%d: incorrect JWT returned from verifiedJWT: want=%#v got=%#v", i, tt.wantJWT, gotJWT)
}
}
}
func TestAuthenticatedTransportJWTCaching(t *testing.T) {
at := &AuthenticatedTransport{
TokenRefresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return errors.New("fail!") },
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "2"}, nil },
},
jwt: jose.JWT{RawPayload: "1"},
}
wantJWT := jose.JWT{RawPayload: "2"}
gotJWT, err := at.verifiedJWT()
if err != nil {
t.Fatalf("got non-nil error: %#v", err)
}
if !reflect.DeepEqual(wantJWT, gotJWT) {
t.Fatalf("incorrect JWT returned from verifiedJWT: want=%#v got=%#v", wantJWT, gotJWT)
}
at.TokenRefresher = &staticTokenRefresher{
verify: func(jose.JWT) error { return nil },
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "3"}, nil },
}
// the previous JWT should still be cached on the AuthenticatedTransport since
// it is still valid, even though there's a new token ready to refresh
gotJWT, err = at.verifiedJWT()
if err != nil {
t.Fatalf("got non-nil error: %#v", err)
}
if !reflect.DeepEqual(wantJWT, gotJWT) {
t.Fatalf("incorrect JWT returned from verifiedJWT: want=%#v got=%#v", wantJWT, gotJWT)
}
}
type fakeRoundTripper struct {
Request *http.Request
resp *http.Response
}
func (r *fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.Request = req
return r.resp, nil
}
func TestAuthenticatedTransportRoundTrip(t *testing.T) {
rr := &fakeRoundTripper{nil, &http.Response{StatusCode: http.StatusOK}}
at := &AuthenticatedTransport{
TokenRefresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return nil },
},
RoundTripper: rr,
jwt: jose.JWT{RawPayload: "1"},
}
req := http.Request{}
_, err := at.RoundTrip(&req)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(req, http.Request{}) {
t.Errorf("http.Request object was modified")
}
want := []string{"Bearer .1."}
got := rr.Request.Header["Authorization"]
if !reflect.DeepEqual(want, got) {
t.Errorf("incorrect Authorization header: want=%#v got=%#v", want, got)
}
}
func TestAuthenticatedTransportRoundTripRefreshFail(t *testing.T) {
rr := &fakeRoundTripper{nil, &http.Response{StatusCode: http.StatusOK}}
at := &AuthenticatedTransport{
TokenRefresher: &staticTokenRefresher{
verify: func(jose.JWT) error { return errors.New("fail!") },
refresh: func() (jose.JWT, error) { return jose.JWT{}, errors.New("fail!") },
},
RoundTripper: rr,
jwt: jose.JWT{RawPayload: "1"},
}
_, err := at.RoundTrip(&http.Request{})
if err == nil {
t.Errorf("expected non-nil error")
}
}

109
vendor/github.com/coreos/go-oidc/oidc/util.go generated vendored Normal file
View file

@ -0,0 +1,109 @@
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)
}

110
vendor/github.com/coreos/go-oidc/oidc/util_test.go generated vendored Normal file
View file

@ -0,0 +1,110 @@
package oidc
import (
"fmt"
"net/http"
"reflect"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
)
func TestCookieTokenExtractorInvalid(t *testing.T) {
ckName := "tokenCookie"
tests := []*http.Cookie{
&http.Cookie{},
&http.Cookie{Name: ckName},
&http.Cookie{Name: ckName, Value: ""},
}
for i, tt := range tests {
r, _ := http.NewRequest("", "", nil)
r.AddCookie(tt)
_, err := CookieTokenExtractor(ckName)(r)
if err == nil {
t.Errorf("case %d: want: error for invalid cookie token, got: no error.", i)
}
}
}
func TestCookieTokenExtractorValid(t *testing.T) {
validToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
ckName := "tokenCookie"
tests := []*http.Cookie{
&http.Cookie{Name: ckName, Value: "some non-empty value"},
&http.Cookie{Name: ckName, Value: validToken},
}
for i, tt := range tests {
r, _ := http.NewRequest("", "", nil)
r.AddCookie(tt)
_, err := CookieTokenExtractor(ckName)(r)
if err != nil {
t.Errorf("case %d: want: valid cookie with no error, got: %v", i, err)
}
}
}
func TestExtractBearerTokenInvalid(t *testing.T) {
tests := []string{"", "x", "Bearer", "xxxxxxx", "Bearer "}
for i, tt := range tests {
r, _ := http.NewRequest("", "", nil)
r.Header.Add("Authorization", tt)
_, err := ExtractBearerToken(r)
if err == nil {
t.Errorf("case %d: want: invalid Authorization header, got: valid Authorization header.", i)
}
}
}
func TestExtractBearerTokenValid(t *testing.T) {
validToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
tests := []string{
fmt.Sprintf("Bearer %s", validToken),
}
for i, tt := range tests {
r, _ := http.NewRequest("", "", nil)
r.Header.Add("Authorization", tt)
_, err := ExtractBearerToken(r)
if err != nil {
t.Errorf("case %d: want: valid Authorization header, got: invalid Authorization header: %v.", i, err)
}
}
}
func TestNewClaims(t *testing.T) {
issAt := time.Date(2, time.January, 1, 0, 0, 0, 0, time.UTC)
expAt := time.Date(2, time.January, 1, 1, 0, 0, 0, time.UTC)
want := jose.Claims{
"iss": "https://example.com",
"sub": "user-123",
"aud": "client-abc",
"iat": issAt.Unix(),
"exp": expAt.Unix(),
}
got := NewClaims("https://example.com", "user-123", "client-abc", issAt, expAt)
if !reflect.DeepEqual(want, got) {
t.Fatalf("want=%#v got=%#v", want, got)
}
want2 := jose.Claims{
"iss": "https://example.com",
"sub": "user-123",
"aud": []string{"client-abc", "client-def"},
"iat": issAt.Unix(),
"exp": expAt.Unix(),
}
got2 := NewClaims("https://example.com", "user-123", []string{"client-abc", "client-def"}, issAt, expAt)
if !reflect.DeepEqual(want2, got2) {
t.Fatalf("want=%#v got=%#v", want2, got2)
}
}

190
vendor/github.com/coreos/go-oidc/oidc/verification.go generated vendored Normal file
View file

@ -0,0 +1,190 @@
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
}

View file

@ -0,0 +1,380 @@
package oidc
import (
"testing"
"time"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
)
func TestVerifyClientClaims(t *testing.T) {
validIss := "https://example.com"
validClientID := "valid-client"
now := time.Now()
tomorrow := now.Add(24 * time.Hour)
header := jose.JOSEHeader{
jose.HeaderKeyAlgorithm: "test-alg",
jose.HeaderKeyID: "1",
}
tests := []struct {
claims jose.Claims
ok bool
}{
// valid token
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": validClientID,
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: true,
},
// valid token, ('aud' claim is []string)
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": []string{"foo", validClientID},
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: true,
},
// valid token, ('aud' claim is []interface{})
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": []interface{}{"foo", validClientID},
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: true,
},
// missing 'iss' claim
{
claims: jose.Claims{
"sub": validClientID,
"aud": validClientID,
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// invalid 'iss' claim
{
claims: jose.Claims{
"iss": "INVALID",
"sub": validClientID,
"aud": validClientID,
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// missing 'sub' claim
{
claims: jose.Claims{
"iss": validIss,
"aud": validClientID,
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// invalid 'sub' claim
{
claims: jose.Claims{
"iss": validIss,
"sub": "INVALID",
"aud": validClientID,
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// missing 'aud' claim
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// invalid 'aud' claim
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": "INVALID",
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// invalid 'aud' claim
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": []string{"INVALID1", "INVALID2"},
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// invalid 'aud' type
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": struct{}{},
"iat": float64(now.Unix()),
"exp": float64(tomorrow.Unix()),
},
ok: false,
},
// expired
{
claims: jose.Claims{
"iss": validIss,
"sub": validClientID,
"aud": validClientID,
"iat": float64(now.Unix()),
"exp": float64(now.Unix()),
},
ok: false,
},
}
for i, tt := range tests {
jwt, err := jose.NewJWT(header, tt.claims)
if err != nil {
t.Fatalf("case %d: Failed to generate JWT, error=%v", i, err)
}
got, err := VerifyClientClaims(jwt, validIss)
if tt.ok {
if err != nil {
t.Errorf("case %d: unexpected error, err=%v", i, err)
}
if got != validClientID {
t.Errorf("case %d: incorrect client ID, want=%s, got=%s", i, validClientID, got)
}
} else if err == nil {
t.Errorf("case %d: expected error but err is nil", i)
}
}
}
func TestJWTVerifier(t *testing.T) {
iss := "http://example.com"
now := time.Now()
future12 := now.Add(12 * time.Hour)
past36 := now.Add(-36 * time.Hour)
past12 := now.Add(-12 * time.Hour)
priv1, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key, error=%v", err)
}
pk1 := *key.NewPublicKey(priv1.JWK())
priv2, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("failed to generate private key, error=%v", err)
}
pk2 := *key.NewPublicKey(priv2.JWK())
newJWT := func(issuer, subject string, aud interface{}, issuedAt, exp time.Time, signer jose.Signer) jose.JWT {
jwt, err := jose.NewSignedJWT(NewClaims(issuer, subject, aud, issuedAt, exp), signer)
if err != nil {
t.Fatal(err)
}
return *jwt
}
tests := []struct {
name string
verifier JWTVerifier
jwt jose.JWT
wantErr bool
}{
{
name: "JWT signed with available key",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{pk1}
},
},
jwt: newJWT(iss, "XXX", "XXX", past12, future12, priv1.Signer()),
wantErr: false,
},
{
name: "JWT signed with available key, with bad claims",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{pk1}
},
},
jwt: newJWT(iss, "XXX", "YYY", past12, future12, priv1.Signer()),
wantErr: true,
},
{
name: "JWT signed with available key",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{pk1}
},
},
jwt: newJWT(iss, "XXX", []string{"YYY", "ZZZ"}, past12, future12, priv1.Signer()),
wantErr: true,
},
{
name: "expired JWT signed with available key",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{pk1}
},
},
jwt: newJWT(iss, "XXX", "XXX", past36, past12, priv1.Signer()),
wantErr: true,
},
{
name: "JWT signed with unrecognized key, verifiable after sync",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() func() []key.PublicKey {
var i int
return func() []key.PublicKey {
defer func() { i++ }()
return [][]key.PublicKey{
[]key.PublicKey{pk1},
[]key.PublicKey{pk2},
}[i]
}
}(),
},
jwt: newJWT(iss, "XXX", "XXX", past36, future12, priv2.Signer()),
wantErr: false,
},
{
name: "JWT signed with unrecognized key, not verifiable after sync",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{pk1}
},
},
jwt: newJWT(iss, "XXX", "XXX", past12, future12, priv2.Signer()),
wantErr: true,
},
{
name: "verifier gets no keys from keysFunc, still not verifiable after sync",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{}
},
},
jwt: newJWT(iss, "XXX", "XXX", past12, future12, priv1.Signer()),
wantErr: true,
},
{
name: "verifier gets no keys from keysFunc, verifiable after sync",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() func() []key.PublicKey {
var i int
return func() []key.PublicKey {
defer func() { i++ }()
return [][]key.PublicKey{
[]key.PublicKey{},
[]key.PublicKey{pk2},
}[i]
}
}(),
},
jwt: newJWT(iss, "XXX", "XXX", past12, future12, priv2.Signer()),
wantErr: false,
},
{
name: "JWT signed with available key, 'aud' is a string array",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error { return nil },
keysFunc: func() []key.PublicKey {
return []key.PublicKey{pk1}
},
},
jwt: newJWT(iss, "XXX", []string{"ZZZ", "XXX"}, past12, future12, priv1.Signer()),
wantErr: false,
},
{
name: "invalid issuer claim shouldn't trigger sync",
verifier: JWTVerifier{
issuer: "example.com",
clientID: "XXX",
syncFunc: func() error {
t.Errorf("invalid issuer claim shouldn't trigger a sync")
return nil
},
keysFunc: func() func() []key.PublicKey {
var i int
return func() []key.PublicKey {
defer func() { i++ }()
return [][]key.PublicKey{
[]key.PublicKey{},
[]key.PublicKey{pk2},
}[i]
}
}(),
},
jwt: newJWT("invalid-issuer", "XXX", []string{"ZZZ", "XXX"}, past12, future12, priv2.Signer()),
wantErr: true,
},
}
for _, tt := range tests {
err := tt.verifier.Verify(tt.jwt)
if tt.wantErr && (err == nil) {
t.Errorf("case %q: wanted non-nil error", tt.name)
} else if !tt.wantErr && (err != nil) {
t.Errorf("case %q: wanted nil error, got %v", tt.name, err)
}
}
}

21
vendor/github.com/coreos/go-oidc/oidc_test.go generated vendored Normal file
View file

@ -0,0 +1,21 @@
package oidc
import (
"net/http"
"testing"
"golang.org/x/net/context"
)
func TestClientContext(t *testing.T) {
myClient := &http.Client{}
ctx := ClientContext(context.Background(), myClient)
gotClient := clientFromContext(ctx)
// Compare pointer values.
if gotClient != myClient {
t.Fatal("clientFromContext did not return the value set by ClientContext")
}
}

15
vendor/github.com/coreos/go-oidc/test generated vendored Executable file
View file

@ -0,0 +1,15 @@
#!/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 $LINTABLE
go vet github.com/coreos/go-oidc

263
vendor/github.com/coreos/go-oidc/verify.go generated vendored Normal file
View file

@ -0,0 +1,263 @@
package oidc
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
// IDTokenVerifier provides verification for ID Tokens.
type IDTokenVerifier struct {
keySet *remoteKeySet
config *verificationConfig
}
// verificationConfig is the unexported configuration for an IDTokenVerifier.
//
// Users interact with this struct using a VerificationOption.
type verificationConfig struct {
issuer string
// If provided, this value must be in the ID Token audiences.
audience string
// If not nil, check the expiry of the id token.
checkExpiry func() time.Time
// If specified, only these sets of algorithms may be used to sign the JWT.
requiredAlgs []string
// If not nil, don't verify nonce.
nonceSource NonceSource
}
// VerificationOption provides additional checks on ID Tokens.
type VerificationOption interface {
// Unexport this method so other packages can't implement this interface.
updateConfig(c *verificationConfig)
}
// 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(options ...VerificationOption) *IDTokenVerifier {
config := &verificationConfig{issuer: p.issuer}
for _, option := range options {
option.updateConfig(config)
}
return newVerifier(p.remoteKeySet, config)
}
func newVerifier(keySet *remoteKeySet, config *verificationConfig) *IDTokenVerifier {
// As discussed in the godocs for VerifrySigningAlg, because almost all providers
// only support RS256, default to only allowing it.
if len(config.requiredAlgs) == 0 {
config.requiredAlgs = []string{RS256}
}
return &IDTokenVerifier{
keySet: keySet,
config: 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 passed as VerifictionOptions, and returns the payload.
//
// 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: mallformed 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,
claims: payload,
}
// Check issuer.
if t.Issuer != v.config.issuer {
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.config.issuer, t.Issuer)
}
// If a client ID has been provided, make sure it's part of the audience.
if v.config.audience != "" {
if !contains(t.Audience, v.config.audience) {
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.audience, t.Audience)
}
}
// If a set of required algorithms has been provided, ensure that the signatures use those.
var keyIDs, gotAlgs []string
for _, sig := range jws.Signatures {
if len(v.config.requiredAlgs) == 0 || contains(v.config.requiredAlgs, sig.Header.Algorithm) {
keyIDs = append(keyIDs, sig.Header.KeyID)
} else {
gotAlgs = append(gotAlgs, sig.Header.Algorithm)
}
}
if len(keyIDs) == 0 {
return nil, fmt.Errorf("oidc: no signatures use a require algorithm, expected %q got %q", v.config.requiredAlgs, gotAlgs)
}
// Get keys from the remote key set. This may trigger a re-sync.
keys, err := v.keySet.keysWithID(ctx, keyIDs)
if err != nil {
return nil, fmt.Errorf("oidc: get keys for id token: %v", err)
}
if len(keys) == 0 {
return nil, fmt.Errorf("oidc: no keys match signature ID(s) %q", keyIDs)
}
// Try to use a key to validate the signature.
var gotPayload []byte
for _, key := range keys {
if p, err := jws.Verify(&key); err == nil {
gotPayload = p
}
}
if len(gotPayload) == 0 {
return nil, fmt.Errorf("oidc: failed to verify id token")
}
// 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")
}
// Check the nonce after we've verified the token. We don't want to allow unverified
// payloads to trigger a nonce lookup.
if v.config.nonceSource != nil {
if err := v.config.nonceSource.ClaimNonce(t.Nonce); err != nil {
return nil, err
}
}
return t, nil
}
// VerifyAudience ensures that an ID Token was issued for the specific client.
//
// Note that a verified token may be valid for other clients, as OpenID Connect allows a token to have
// multiple audiences.
func VerifyAudience(clientID string) VerificationOption {
return clientVerifier{clientID}
}
type clientVerifier struct {
clientID string
}
func (v clientVerifier) updateConfig(c *verificationConfig) {
c.audience = v.clientID
}
// VerifyExpiry ensures that an ID Token has not expired.
func VerifyExpiry() VerificationOption {
return expiryVerifier{}
}
type expiryVerifier struct{}
func (v expiryVerifier) updateConfig(c *verificationConfig) {
c.checkExpiry = time.Now
}
// VerifySigningAlg enforces that an ID Token is signed by a specific signing algorithm.
//
// Because so many providers only support RS256, if this verifiction option isn't used,
// the IDTokenVerifier defaults to only allowing RS256.
func VerifySigningAlg(allowedAlgs ...string) VerificationOption {
return algVerifier{allowedAlgs}
}
type algVerifier struct {
algs []string
}
func (v algVerifier) updateConfig(c *verificationConfig) {
c.requiredAlgs = v.algs
}
// 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)
}
// NonceSource represents a source which can verify a nonce is valid and has not
// been claimed before.
type NonceSource interface {
ClaimNonce(nonce string) error
}
// VerifyNonce ensures that the ID Token contains a nonce which can be claimed by the nonce source.
func VerifyNonce(source NonceSource) VerificationOption {
return nonceVerifier{source}
}
type nonceVerifier struct {
nonceSource NonceSource
}
func (n nonceVerifier) updateConfig(c *verificationConfig) {
c.nonceSource = n.nonceSource
}

265
vendor/github.com/coreos/go-oidc/verify_test.go generated vendored Normal file
View file

@ -0,0 +1,265 @@
package oidc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/json"
"net/http/httptest"
"testing"
"time"
"golang.org/x/net/context"
jose "gopkg.in/square/go-jose.v2"
)
func TestVerify(t *testing.T) {
tests := []verificationTest{
{
name: "good token",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
},
signKey: testKeyRSA_2048_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
},
{
name: "invalid signature",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
},
signKey: testKeyRSA_2048_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_1},
wantErr: true,
},
{
name: "invalid issuer",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://bar",
},
signKey: testKeyRSA_2048_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
wantErr: true,
},
}
for _, test := range tests {
test.run(t)
}
}
func TestVerifyAudience(t *testing.T) {
tests := []verificationTest{
{
name: "good audience",
idToken: idToken{
Issuer: "https://foo",
Audience: []string{"client1"},
},
config: verificationConfig{
issuer: "https://foo",
audience: "client1",
},
signKey: testKeyRSA_2048_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
},
{
name: "mismatched audience",
idToken: idToken{
Issuer: "https://foo",
Audience: []string{"client2"},
},
config: verificationConfig{
issuer: "https://foo",
audience: "client1",
},
signKey: testKeyRSA_2048_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
wantErr: true,
},
{
name: "multiple audiences, one matches",
idToken: idToken{
Issuer: "https://foo",
Audience: []string{"client2", "client1"},
},
config: verificationConfig{
issuer: "https://foo",
audience: "client1",
},
signKey: testKeyRSA_2048_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
},
}
for _, test := range tests {
test.run(t)
}
}
func TestVerifySigningAlg(t *testing.T) {
tests := []verificationTest{
{
name: "default signing alg",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
},
signKey: testKeyRSA_2048_0_Priv,
signAlg: RS256, // By default we only support RS256.
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
},
{
name: "bad signing alg",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
},
signKey: testKeyRSA_2048_0_Priv,
signAlg: RS512,
pubKeys: []jose.JSONWebKey{testKeyRSA_2048_0},
wantErr: true,
},
{
name: "ecdsa signing",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
requiredAlgs: []string{ES384},
},
signAlg: ES384,
signKey: testKeyECDSA_384_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyECDSA_384_0},
},
{
name: "one of many supported",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
requiredAlgs: []string{RS256, ES384},
},
signAlg: ES384,
signKey: testKeyECDSA_384_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyECDSA_384_0},
},
{
name: "not in requiredAlgs",
idToken: idToken{
Issuer: "https://foo",
},
config: verificationConfig{
issuer: "https://foo",
requiredAlgs: []string{RS256, ES512},
},
signAlg: ES384,
signKey: testKeyECDSA_384_0_Priv,
pubKeys: []jose.JSONWebKey{testKeyECDSA_384_0},
wantErr: true,
},
}
for _, test := range tests {
test.run(t)
}
}
type verificationTest struct {
name string
// ID token claims and a signing key to create the JWT.
idToken idToken
signKey jose.JSONWebKey
// If supplied use this signing algorithm. If not, guess
// from the signingKey.
signAlg string
config verificationConfig
pubKeys []jose.JSONWebKey
wantErr bool
}
func algForKey(t *testing.T, k jose.JSONWebKey) string {
switch key := k.Key.(type) {
case *rsa.PrivateKey:
return RS256
case *ecdsa.PrivateKey:
name := key.PublicKey.Params().Name
switch name {
case elliptic.P256().Params().Name:
return ES256
case elliptic.P384().Params().Name:
return ES384
case elliptic.P521().Params().Name:
return ES512
}
t.Fatalf("unsupported ecdsa curve: %s", name)
default:
t.Fatalf("unsupported key type %T", key)
}
return ""
}
func (v verificationTest) run(t *testing.T) {
payload, err := json.Marshal(v.idToken)
if err != nil {
t.Fatal(err)
}
signingAlg := v.signAlg
if signingAlg == "" {
signingAlg = algForKey(t, v.signKey)
}
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(signingAlg),
Key: &v.signKey,
}, nil)
if err != nil {
t.Fatal(err)
}
jws, err := signer.Sign(payload)
if err != nil {
t.Fatal(err)
}
token, err := jws.CompactSerialize()
if err != nil {
t.Fatal(err)
}
t0 := time.Now()
now := func() time.Time { return t0 }
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := httptest.NewServer(newKeyServer(v.pubKeys...))
defer server.Close()
verifier := newVerifier(newRemoteKeySet(ctx, server.URL, now), &v.config)
if _, err := verifier.Verify(ctx, token); err != nil {
if !v.wantErr {
t.Errorf("%s: verify %v", v.name, err)
}
} else {
if v.wantErr {
t.Errorf("%s: expected error", v.name)
}
}
}

View file

@ -1,13 +0,0 @@
language: go
go:
- 1.5.4
- 1.6.3
- tip
notifications:
email: false
matrix:
allow_failures:
- go: tip

View file

@ -1,106 +0,0 @@
# OpenID Connect client support for Go
[![GoDoc](https://godoc.org/github.com/ericchiang/oidc?status.svg)](https://godoc.org/github.com/ericchiang/oidc)
This package implements OpenID Connect client logic for the golang.org/x/oauth2 package.
```go
provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
if err != nil {
return err
}
// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Endpoint: provider.Endpoint(),
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)
})
```
For callbacks the provider can be used to query for [user information](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) such as email.
```go
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
userinfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
// ...
})
```
Or the provider can be used to verify and inspect the OpenID Connect
[ID Token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) in the
[token response](https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse).
```go
verifier := provider.NewVerifier(ctx)
```
The verifier itself can be constructed with addition checks, such as verifing a
token was issued for a specific client or hasn't expired.
```go
verifier := provier.NewVerifier(ctx, oidc.VerifyAudience(clientID), oidc.VerifyExpiry())
```
The returned verifier can be used to ensure the ID Token (a JWT) is signed by the provider.
```go
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
// Extract the ID Token from oauth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No ID Token found", http.StatusInternalServerError)
return
}
// Verify that the ID Token is signed by the provider.
idToken, err := verifier.Verify(rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
// Unmarshal ID Token for expected custom claims.
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, "Failed to unmarshal ID Token claims: "+err.Error(), http.StatusInternalServerError)
return
}
// ...
})
```

View file

@ -1,145 +0,0 @@
/*
Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
if err != nil {
return err
}
// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
OAuth2 redirects are unchanged.
func handleRedirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
})
For callbacks the provider can be used to query for user information such as email.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
userinfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
// ...
})
The provider also has the ability to verify ID Tokens.
verifier := provider.NewVerifier(ctx)
The returned verifier can be used to perform basic validation on ID Token issued by the provider,
including verifying the JWT signature. It then returns the payload.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
// Extract the ID Token from oauth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No ID Token found", http.StatusInternalServerError)
return
}
// Verify that the ID Token is signed by the provider.
idToken, err := verifier.Verify(rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
// Unmarshal ID Token for expected custom claims.
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, "Failed to unmarshal ID Token custom claims: "+err.Error(), http.StatusInternalServerError)
return
}
// ...
})
ID Token nonces are supported.
First, provide a nonce source for nonce validation. This will then be used to wrap the existing
provider ID Token verifier.
// A verifier which boths verifies the ID Token signature and nonce.
nonceEnabledVerifier := provider.NewVerifier(ctx, oidc.VerifyNonce(nonceSource))
For the redirect provide a nonce auth code option. This will be placed as a URL parameter during
the client redirect.
func handleRedirect(w http.ResponseWriter, r *http.Request) {
nonce, err := newNonce()
if err != nil {
// ...
}
// Provide a nonce for the OpenID Connect ID Token.
http.Redirect(w, r, oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound)
})
The nonce enabled verifier can then be used to verify the nonce while unpacking the ID Token.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
// Extract the ID Token from oauth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No ID Token found", http.StatusInternalServerError)
return
}
// Verify that the ID Token is signed by the provider and verify the nonce.
idToken, err := nonceEnabledVerifier.Verify(rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
// Continue as above...
})
This package uses contexts to derive HTTP clients in the same way as the oauth2 package. To configure
a custom client, use the oauth2 packages HTTPClient context key when constructing the context.
myClient := &http.Client{}
myCtx := context.WithValue(parentCtx, oauth2.HTTPClient, myClient)
// NewProvider will use myClient to make the request.
provider, err := oidc.NewProvider(myCtx, "https://accounts.example.com")
*/
package oidc

View file

@ -1,15 +0,0 @@
# Examples
These are example uses of the oidc package. Each requires a Google account and the
client ID and secret of a registered OAuth2 application. The client ID and secret
should be set as the following environment variables:
```
GOOGLE_OAUTH2_CLIENT_ID
GOOGLE_OAUTH2_CLIENT_SECRET
```
See Google's documentation on how to set up an OAuth2 app:
https://developers.google.com/identity/protocols/OpenIDConnect?hl=en
Note that one of the redirect URL's must be "http://127.0.0.1:5556/auth/google/callback"

View file

@ -1,7 +0,0 @@
// Package internal contains support packages for the oidc package.
package internal
// ContextKey is just an empty struct. It exists so context keys 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{}

View file

@ -1,188 +0,0 @@
package oidc
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/pquerna/cachecontrol"
"golang.org/x/net/context"
jose "gopkg.in/square/go-jose.v1"
)
// No matter what insist on caching keys. This is so our request code can be
// asynchronous from matching keys. If the request code retrieved keys that
// expired immediately, the goroutine to match a JWT to a key would always see
// expired keys.
//
// TODO(ericchiang): Review this logic.
var minCache = 2 * time.Minute
type cachedKeys struct {
keys map[string]jose.JsonWebKey // immutable
expiry time.Time
}
type remoteKeySet struct {
client *http.Client
// "jwks_uri" from discovery.
keysURL string
// The value is always of type *cachedKeys.
//
// To ensure consistency always call keyCache.Store when holding cond.L.
keyCache atomic.Value
// cond.L guards all following fields. sync.Cond is used in place of a mutex
// so multiple processes can wait on a single request to update keys.
cond sync.Cond
// Is there an existing request to get the remote keys?
inflight bool
// If the last attempt to refresh keys failed, the error will be saved here.
//
// TODO(ericchiang): If a routine sets this before calling cond.Broadcast(),
// there's no guarentee that a routine calling cond.Wait() will actual see
// the error called by the previous routine. Since Broadcast() unlocks
// cond.L and Wait() must reacquire the lock, other routines waiting on the
// lock might acquire it first. Maybe just log the error?
lastErr error
}
func newRemoteKeySet(ctx context.Context, jwksURL string) *remoteKeySet {
r := &remoteKeySet{
client: contextClient(ctx),
keysURL: jwksURL,
cond: sync.Cond{L: new(sync.Mutex)},
}
return r
}
func (r *remoteKeySet) verifyJWT(jwt string) (payload []byte, err error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("parsing jwt: %v", err)
}
keyIDs := make([]string, len(jws.Signatures))
for i, signature := range jws.Signatures {
keyIDs[i] = signature.Header.KeyID
}
key, err := r.getKey(keyIDs)
if err != nil {
return nil, fmt.Errorf("oidc: %s", err)
}
return jws.Verify(key)
}
func (r *remoteKeySet) getKeyFromCache(keyIDs []string) (*jose.JsonWebKey, bool) {
cachedKeys, ok := r.keyCache.Load().(*cachedKeys)
if !ok {
return nil, false
}
if time.Now().After(cachedKeys.expiry) {
return nil, false
}
for _, keyID := range keyIDs {
if key, ok := cachedKeys.keys[keyID]; ok {
return &key, true
}
}
return nil, false
}
func (r *remoteKeySet) getKey(keyIDs []string) (*jose.JsonWebKey, error) {
// Fast path. Just do an atomic load.
if key, ok := r.getKeyFromCache(keyIDs); ok {
return key, nil
}
// Didn't find keys, use the slow path.
r.cond.L.Lock()
defer r.cond.L.Unlock()
// Check again within the mutex.
if key, ok := r.getKeyFromCache(keyIDs); ok {
return key, nil
}
// Keys have expired or we're trying to verify a JWT we don't have a key for.
if !r.inflight {
// There isn't currently an inflight request to update keys, start a
// goroutine to do so.
r.inflight = true
go func() {
newKeys, newExpiry, err := requestKeys(r.client, r.keysURL)
r.cond.L.Lock()
defer r.cond.L.Unlock()
r.inflight = false
if err != nil {
r.lastErr = err
} else {
r.keyCache.Store(&cachedKeys{newKeys, newExpiry})
r.lastErr = nil
}
r.cond.Broadcast() // Wake all r.cond.Wait() calls.
}()
}
// Wait for r.cond.Broadcast() to be called. This unlocks r.cond.L and
// reacquires it after its done waiting.
r.cond.Wait()
if key, ok := r.getKeyFromCache(keyIDs); ok {
return key, nil
}
if r.lastErr != nil {
return nil, r.lastErr
}
return nil, errors.New("no signing keys can validate the signature")
}
func requestKeys(client *http.Client, keysURL string) (map[string]jose.JsonWebKey, time.Time, error) {
req, err := http.NewRequest("GET", keysURL, nil)
if err != nil {
return nil, time.Time{}, fmt.Errorf("can't create request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, time.Time{}, fmt.Errorf("can't GET new keys %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, time.Time{}, fmt.Errorf("can't fetch new keys: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, time.Time{}, fmt.Errorf("can't fetch new keys: %s %s", resp.Status, body)
}
var keySet jose.JsonWebKeySet
if err := json.Unmarshal(body, &keySet); err != nil {
return nil, time.Time{}, fmt.Errorf("can't decode keys: %v %s", err, body)
}
keys := make(map[string]jose.JsonWebKey, len(keySet.Keys))
for _, key := range keySet.Keys {
keys[key.KeyID] = key
}
minExpiry := time.Now().Add(minCache)
if _, expiry, err := cachecontrol.CachableResponse(req, resp, cachecontrol.Options{}); err == nil {
if minExpiry.Before(expiry) {
return keys, expiry, nil
}
}
return keys, minExpiry, nil
}

File diff suppressed because one or more lines are too long

View file

@ -1,35 +0,0 @@
package oidc
import (
"errors"
"golang.org/x/oauth2"
)
// 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)
}
// NonceSource represents a source which can verify a nonce is valid and has not
// been claimed before.
type NonceSource interface {
ClaimNonce(nonce string) error
}
// VerifyNonce ensures that the ID Token contains a nonce which can be claimed by the nonce source.
func VerifyNonce(source NonceSource) VerificationOption {
return nonceVerifier{source}
}
type nonceVerifier struct {
nonceSource NonceSource
}
func (n nonceVerifier) verifyIDToken(token *IDToken) error {
if token.Nonce == "" {
return errors.New("oidc: no nonce present in ID Token")
}
return n.nonceSource.ClaimNonce(token.Nonce)
}

View file

@ -1,76 +0,0 @@
package oidc
import (
"encoding/json"
"reflect"
"testing"
)
func TestClientVerifier(t *testing.T) {
tests := []struct {
clientID string
aud []string
wantErr bool
}{
{
clientID: "1",
aud: []string{"1"},
},
{
clientID: "1",
aud: []string{"2"},
wantErr: true,
},
{
clientID: "1",
aud: []string{"2", "1"},
},
{
clientID: "3",
aud: []string{"1", "2"},
wantErr: true,
},
}
for i, tc := range tests {
token := IDToken{Audience: tc.aud}
err := (clientVerifier{tc.clientID}).verifyIDToken(&token)
if err != nil && !tc.wantErr {
t.Errorf("case %d: %v", i)
}
if err == nil && tc.wantErr {
t.Errorf("case %d: expected error")
}
}
}
func TestUnmarshalAudience(t *testing.T) {
tests := []struct {
data string
want audience
wantErr bool
}{
{`"foo"`, audience{"foo"}, false},
{`["foo","bar"]`, audience{"foo", "bar"}, false},
{"foo", nil, true}, // invalid JSON
}
for _, tc := range tests {
var a audience
if err := json.Unmarshal([]byte(tc.data), &a); err != nil {
if !tc.wantErr {
t.Errorf("failed to unmarshal %q: %v", tc.data, err)
}
continue
}
if tc.wantErr {
t.Errorf("did not expected to be able to unmarshal %q", tc.data)
continue
}
if !reflect.DeepEqual(tc.want, a) {
t.Errorf("from %q expected %q got %q", tc.data, tc.want, a)
}
}
}

View file

@ -1,283 +0,0 @@
package main
import (
"crypto/rand"
"encoding/gob"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/ericchiang/oidc"
"github.com/gorilla/securecookie"
"golang.org/x/net/context"
"golang.org/x/oauth2"
)
const (
cookieName = "oidc-proxy"
// This header will be set by oidcproxy during authentication and
// passed to the backend.
emailHeaderName = "X-User-Email"
)
// Session represents a logged in user's active session.
type Session struct {
Email string
Expires time.Time
}
func init() {
gob.Register(&Session{})
}
var (
// Flags.
issuer string
backend string
scopes string
allow string
httpAddr string
httpsAddr string
cookieExp time.Duration
// Set up during initial configuration.
oauth2Config = new(oauth2.Config)
oidcProvider *oidc.Provider
backendHandler *httputil.ReverseProxy
verifier *oidc.IDTokenVerifier
// Regexps of emails to allow.
allowEmail []*regexp.Regexp
nonceSource *memNonceSource
cookieEncrypter *securecookie.SecureCookie
)
func main() {
flag.StringVar(&issuer, "issuer", "https://accounts.google.com", "The issuer URL of the OpenID Connect provider.")
flag.StringVar(&backend, "backend", "", "The URL of the backened to proxy to.")
flag.StringVar(&oauth2Config.RedirectURL, "redirect-url", "", "A full OAuth2 redirect URL.")
flag.StringVar(&oauth2Config.ClientID, "client-id", "", "The client ID of the OAuth2 client.")
flag.StringVar(&oauth2Config.ClientSecret, "client-secret", "", "The client secret of the OAuth2 client.")
flag.StringVar(&scopes, "scopes", "openid,email,profile", `A comma seprated list of OAuth2 scopes to request ("openid" required).`)
flag.StringVar(&allow, "allow-email", ".*", "Comma seperated list of email regexp's to match for access to the backend.")
flag.StringVar(&httpAddr, "http", "127.0.0.1:5556", "Default address to listen on.")
flag.DurationVar(&cookieExp, "cookie-exp", time.Hour*24, "Duration for which a login cookie is valid for.")
flag.Parse()
// Set flags from environment variables.
flag.VisitAll(func(f *flag.Flag) {
if f.Value.String() != f.DefValue {
return
}
// Convert flag name, e.g. "redirect-url" becomes "OIDC_PROXY_REDIRECT_URL"
envVar := "OIDC_PROXY_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1))
if envVal := os.Getenv(envVar); envVal != "" {
if err := flag.Set(f.Name, envVal); err != nil {
log.Fatal(err)
}
}
// All flags are manditory.
if f.Value.String() == "" {
flag.Usage()
os.Exit(2)
}
})
// compile email regexps
for _, expr := range strings.Split(allow, ",") {
allowEmailRegexp, err := regexp.Compile(expr)
if err != nil {
log.Fatalf("invalid regexp: %q %v", expr, err)
}
allowEmail = append(allowEmail, allowEmailRegexp)
}
// configure reverse proxy
backendURL, err := url.Parse(backend)
if err != nil {
log.Fatalf("failed to parse backend: %v", err)
}
backendHandler = httputil.NewSingleHostReverseProxy(backendURL)
redirectURL, err := url.Parse(oauth2Config.RedirectURL)
if err != nil {
log.Fatalf("failed to parse redirect URL: %v", err)
}
// Query for the provider.
oidcProvider, err = oidc.NewProvider(context.TODO(), issuer)
if err != nil {
log.Fatalf("failed to get provider: %v", err)
}
nonceSource = newNonceSource(context.TODO())
verifier = oidcProvider.NewVerifier(context.TODO(), oidc.VerifyNonce(nonceSource))
oauth2Config.Endpoint = oidcProvider.Endpoint()
oauth2Config.Scopes = strings.Split(scopes, ",")
// Initialize secure cookies.
// TODO(ericchiang): make these configurable
hashKey := make([]byte, 64)
blockKey := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, hashKey); err != nil {
log.Fatalf("failed to initialize hash key: %v", err)
}
if _, err := io.ReadFull(rand.Reader, blockKey); err != nil {
log.Fatalf("failed to initialize block key: %v", err)
}
cookieEncrypter = securecookie.New(hashKey, blockKey)
mux := http.NewServeMux()
mux.HandleFunc("/", handleProxy)
mux.HandleFunc("/login", handleRedirect)
mux.HandleFunc("/logout", handleLogout)
mux.HandleFunc(redirectURL.Path, handleCallback)
log.Printf("Listening on: %s", httpAddr)
http.ListenAndServe(httpAddr, mux)
}
// httpRedirect returns a handler which redirects to the provided path.
func httpRedirect(path string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path, http.StatusFound)
})
}
// httpError returns a handler which presents an error to the end user.
func httpError(status int, format string, a ...interface{}) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf(format, a...), http.StatusInternalServerError)
})
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
func() http.Handler {
state := r.URL.Query().Get("state")
if state == "" {
log.Printf("State not set")
return httpError(http.StatusInternalServerError, "Authentication failed")
}
if err := nonceSource.ClaimNonce(state); err != nil {
log.Printf("Failed to claim nonce: %v", err)
return httpError(http.StatusInternalServerError, "Authentication failed")
}
oauth2Token, err := oauth2Config.Exchange(context.TODO(), r.URL.Query().Get("code"))
if err != nil {
log.Printf("Failed to exchange token: %v", err)
return httpError(http.StatusInternalServerError, "Authentication failed")
}
// Extract the ID Token from oauth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
log.Println("No ID Token found")
return httpError(http.StatusInternalServerError, "Authentication failed")
}
idToken, err := verifier.Verify(rawIDToken)
if err != nil {
log.Printf("Failed to verify token: %v", err)
return httpError(http.StatusInternalServerError, "Authentication failed")
}
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
log.Printf("Failed to decode claims: %v", err)
return httpError(http.StatusInternalServerError, "Authentication failed")
}
if !claims.EmailVerified || claims.Email == "" {
log.Println("Failed to verify email")
return httpError(http.StatusInternalServerError, "Authentication failed")
}
s := Session{Email: claims.Email, Expires: time.Now().Add(cookieExp)}
encoded, err := cookieEncrypter.Encode(cookieName, s)
if err != nil {
log.Printf("Failed to encrypt session: %v", err)
return httpError(http.StatusInternalServerError, "Authentication failed")
}
// Set the encoded cookie
cookie := &http.Cookie{Name: cookieName, Value: encoded, HttpOnly: true, Path: "/"}
http.SetCookie(w, cookie)
return httpRedirect("/")
}().ServeHTTP(w, r)
}
func handleRedirect(w http.ResponseWriter, r *http.Request) {
// TODO(ericchiang): since arbitrary requests can create nonces, rate limit this endpoint.
func() http.Handler {
nonce, err := nonceSource.Nonce()
if err != nil {
log.Printf("Failed to create nonce: %v", err)
return httpError(http.StatusInternalServerError, "Failed to generate redirect")
}
state, err := nonceSource.Nonce()
if err != nil {
log.Printf("Failed to create state: %v", err)
return httpError(http.StatusInternalServerError, "Failed to generate redirect")
}
return httpRedirect(oauth2Config.AuthCodeURL(state, oauth2.ApprovalForce, oidc.Nonce(nonce)))
}().ServeHTTP(w, r)
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{Name: cookieName, Value: "", HttpOnly: true, Path: "/"}
http.SetCookie(w, cookie)
httpRedirect("/login").ServeHTTP(w, r)
}
func handleProxy(w http.ResponseWriter, r *http.Request) {
func() http.Handler {
cookie, err := r.Cookie(cookieName)
if err != nil {
// Only error can be ErrNoCookie https://goo.gl/o5fZ49
return httpRedirect("/login")
}
var s Session
if err := cookieEncrypter.Decode(cookieName, cookie.Value, &s); err != nil {
log.Printf("Failed to decode cookie: %v", err)
return http.HandlerFunc(handleLogout) // clear the cookie
}
if time.Now().After(s.Expires) {
log.Printf("Cookie for %q expired", s.Email)
return http.HandlerFunc(handleLogout) // clear the cookie
}
for _, allow := range allowEmail {
if allow.MatchString(s.Email) {
r.Header.Set(emailHeaderName, s.Email)
return backendHandler
}
}
log.Printf("Denying %q", s.Email)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`<html><head></head><body>Provided email does not have permission to login. <a href="/logout">Try a different account.</a></body></html>`)
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Content-Length", strconv.Itoa(len(resp)))
w.WriteHeader(http.StatusForbidden)
w.Write(resp)
})
}().ServeHTTP(w, r)
}

View file

@ -1,72 +0,0 @@
package main
import (
"crypto/rand"
"encoding/base64"
"errors"
"io"
"sync"
"time"
"golang.org/x/net/context"
)
var (
gcInterval = time.Minute
expiresIn = time.Minute * 10
)
type memNonceSource struct {
mu sync.Mutex
nonces map[string]time.Time
}
func newNonceSource(ctx context.Context) *memNonceSource {
s := &memNonceSource{nonces: make(map[string]time.Time)}
go s.garbageCollect(ctx)
return s
}
func (s *memNonceSource) Nonce() (string, error) {
buff := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, buff); err != nil {
return "", err
}
nonce := base64.RawURLEncoding.EncodeToString(buff)
s.mu.Lock()
defer s.mu.Unlock()
s.nonces[nonce] = time.Now().Add(expiresIn)
return nonce, nil
}
func (s *memNonceSource) ClaimNonce(nonce string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.nonces[nonce]; ok {
delete(s.nonces, nonce)
return nil
}
return errors.New("invalid nonce")
}
func (s *memNonceSource) garbageCollect(ctx context.Context) {
for {
select {
case <-ctx.Done():
case <-time.After(gcInterval):
s.mu.Lock()
now := time.Now()
for nonce, exp := range s.nonces {
if now.After(exp) {
delete(s.nonces, nonce)
}
}
s.mu.Unlock()
}
}
}

View file

@ -1,7 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIA3zFNhDsB70vMBOzeK48Zn6oic5wfx9xto4ErduEVKFST0aJEfjLO
/kzNrKDgXArCEl2KYJQfb8J9lslA7cLvpVSgBwYFK4EEACOhgYkDgYYABABxyN4Y
6VxH/86lgejSlHGrjKVSzn6YeOukabBSiU8PS/o/wfGXKX4eKCkJYqVq18zGAfcL
q+UM09ZQv/De7mGkXwC67qQv7fS7tJ/t0uFcxriQNtVGPsL4/+YmWrFJBTlK0OgD
mkSBG7ERdb5x/JzNFbajSbX0wKzs4VOZU0VVj/2DJw==
-----END EC PRIVATE KEY-----

View file

@ -1,7 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIAtlsqZlQW4bWtSmDgLjbhcCgmmWEVwtzHMTZRoQnUv4rdTxCeKLY3
hjxzd5hPCmtfP68GyJhKQgKofKYD/DgQLc2gBwYFK4EEACOhgYkDgYYABAABrcbo
t8KEfgzslg4Bb7t0khCgFrT2hX5htSWnwwHiScs1yO9egRcftZg/WAoIo/QDID+i
OB4f5Flg5PygZpm/SwE4B1E8dGpyLRmBg3cC0/PfuRkGZ2E5POKZqsiRU5TkvC2D
AkwVr6UPpXPheStrp2qh6ptBUtZRzn8Q4lVFKoKe9g==
-----END EC PRIVATE KEY-----

View file

@ -1,7 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIAzUq62J5hiC8B5xw9M/e9KlSeO66uq9PHRSGcFY4d9MFgFKILKU0u
cfUBCwbhOnDWkdUTp1DkLWeNhE0UUvN2FgegBwYFK4EEACOhgYkDgYYABAHH7ZyR
YXJ9oDJ/KohiQFFXqkvspk3ljvBAFUFRyfL4Q40TtgKGt5YBNmU3SHNHn3fJdjQy
xe2OZcmKYxzwCjx6mgACI0IoiIdgZN0RBy8UMhgn810C/iDg+nOZScl7P0t3DFcv
H3K5+tkVWe8lLBIUOkqEyHmmfHGYcn6Kc5jHEnAebA==
-----END EC PRIVATE KEY-----

View file

@ -1,7 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIBt+T9wRbTfN3T5kSqfT5nqCt65w+SGAQ5DXQgcf7gCXId+Ux/57MA
/Dld+PvG+T8mobr1/jaFiGOLLRsjtnc5Ml+gBwYFK4EEACOhgYkDgYYABAE/ka2T
p7MsBezSgeATljES2xBY4wDOcjMmI6MzHdiO9hU/xcIQnhc2tjML2QZSMTuLy1ZQ
Yjhu0ZRg5Dxj4m7mgAFp2f/FqtOSAR5vuikaYPzHwosvNFIIpRDJCZ23j6qbtemF
5qXUlSXf2+W491rfb2njNwTWx8BLn1M3fFhobK+O9Q==
-----END EC PRIVATE KEY-----

View file

@ -1,15 +0,0 @@
#!/bin/bash
set -e
for i in $(seq 1 4); do
openssl ecparam -out ecdsa_521_${i}.pem -name secp521r1 -genkey -noout
done
for i in $(seq 1 4); do
openssl genrsa -out rsa_2048_${i}.pem 2048
done
for i in $(seq 1 4); do
openssl genrsa -out rsa_4096_${i}.pem 4096
done

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA05kMh0SJfLTbcAhpq1w8jpTuo+Shy95WgvYc1KzxcH6ol/nq
vFSn1VCT/OHyg1t8BGeIfeuckIppT+cRtTjrsqS6FEmJA3lAiRqJZLWbewyXgoaZ
eCsmMS0s3w8KUon4Z9t8rFVqv2fkki6p7FtLPjPD0PsTRNCPwtOGM8Ci+0qFuCrt
4flUr6DpiALkqN1PSJCAwL22y8C86S6PPBU2seR3TWdD+iMAmr3Rezh+J/JqYXK+
4qORzE6mA3hjn+goULQif5fUbYG0vRSyEp8UrlzevS88+ZzxyS9iTZ6H1ympLcsV
PeqvCIXPd1OJXn6ZGSuIgOlgZuaieKeuYLHDCQIDAQABAoIBAHrxC+R0H+YDNxRq
7uqPlufJBLbZGmDXeDBzSuEO8uFH1jEnFgoCrdk1Dib6KOvFddMhTJ7NDJS2tuWj
/hfrUJblOvCaoS8Rfjuq3XVUR1hBQq6mAfleKLyd4NphZL/8RgYh8tg2cOVxOc7t
qfEYQimL7hQ4LUPoYf7y46CiJpAV+BIwrR74k9Y3vcpumLwWrkwlfLTMWmcaiJE+
gQBVl5+CQZmVKohTPDfCnQ9+ISzxvh4nesiQORMljG/ssQaZi5h7VEJDqGOaDgVB
CsFp9fxLrQzx1Gjxv/uEhG/k45uAU1qgNZcL3/XQhyCgadUsuMM3yOndIaiQNbNN
7bm1b9kCgYEA6XICJjg6vRbIO7nS5VYW1efFhLlqYjFimjv5f5vaTWA3FM8S1crL
HG4Q8yh+CBOwY0mtcwr8RjlnX5Dsi+wGJgFjaL7OG0MDojv9/YKZse4dIHELUPKC
Wj1zxiE23JsMAWKXvkhgGJgkC4mksZHVVszcLOEvmn2uZghDtTxhpw8CgYEA6Aqr
NipU2MPPOe0Hu2Ar6Kb/EaJyJA2G8lABWILwyI6Wv13dpZz1FfjcpnsZVRNBuYla
O3OKZun537kQOfHMWJfZyj2fOrJ0z3rsWZ+nbbUy1um8Z6jHTPqQyNoelE5/QTvs
CJ0jDzotbUYsvE4TdOH2EzSneecAgBN7Brz/tGcCgYEAuSpcOBKbzMZYVr+DX7NU
c6Dek/M6Rd6kNnBh620k0AEET7YcW4X6a3eGbEjvBtsPKwIS2VCaX91CeJQMfMPe
8KBjSH8oHomeRT3Orhm8bVzQr53a+v8QlCFwRnSr/nnhIOwiLqVby8ZJuPkZsFtb
W/ksn1CSoLkV7wqZIhVd49MCgYEArkFU0hh4H1DtDlMyu0Q9tTmz00pq7Sg7bz0l
xZKPwA1Up+GV0glNBHMfQOaw33LWqL69RGhAR4juXVRdGya6js16gKZGLY5Wqnll
hOigk4K/6yUcl7vn76c7k5o53KYWaqbVWqKm8Yh/FNDeR4takSwf38xq+ODBP21h
tm24mYECgYBUS4Ox2dzDBPIKVfxyuV+FviM9wYKBd9G2xUYavF5Id/0byYOkOy11
K9L4NJI4Xztzw6KZw7ngUsBmK9AN60mLO1SkHyMr7dLanyt03X46jhtEQem+wDqf
HgKcqIz0gxaU6+widaEM33/cTi+kafH2uLr13aHU23ZfMV7oeRk3XQ==
-----END RSA PRIVATE KEY-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAsB/GrXd+Ygz/Gq0nxKX06cLh4ECe+AfINVz+LdBCY1R8ossV
1v2Rk6M59EMZwsR3XuzCoioEGzI+QyEUUQO+FpsDMpZ3iL4+dTCZIo9XXfHyQEnq
JCy2w6Al2Xr6qTvfHBCMn61IGwAgDhDDb0djclJhqLhgxAVwXyxt3+pVSnUqdKtt
h0tgLqyEgqX19QjDEh0xhFT+zB/IHmolaJO1DelvDLYmyaxoOCizGb2WyMcjkZ6m
sdV/rYpzBW1BjA9N1+gnTsPF1cQ6wJJTtDRjovKWtqCQSmmPnQSVNwIz8+zI9FUc
qPM6gP6v56bcpmqPtPylY6T3GbGxBQtW+1Z2/QIDAQABAoIBAFNvikiNTlMXAxdZ
JnjTgfXn++en1Wd9EEyvdD6x5XF3CeB5QyxpTbjaX88mpqKNPlu63+3A59cWc0aL
+jrzAe9lmhsyCwi9z4rm7fTgYSxBPVlVatWeVSrRyHyB9RONKIH8GRJgHcOkyIrB
SESEVklHW7p5NmZGiVidDKRCOAugMpnYbOB2Nf7wI7cxHT8QcxcKFaQTdCSnYv0D
eMWpEmP8vsEmnme8Q6Uax470yBHQQvI7JfWUIbh3wZVdDLllbr+E17ej8EHatCyD
A6J1lLZ72DJy37G/n3kLLtHy0oWjVk9ZT2m+HEQHSEnAqgHz0UzGKGVoeSnbAxUw
FATBNYUCgYEA3KowvUgRyc1ydi2L6YqQQg8Mhq6SdSPkC91p3pI8pMJGwco0tI2y
95EFMsFUg3m+v2URDKxjt3itx+vqWxSqeQBgUzKk+0TRuKt0+8dav5BUA3njtLEk
VdlzwofbvagUnE+slg0lQKsK/IzODPCckcOjBcUyJI1PNYdDjfQjv7cCgYEAzFO1
vtexZEUw1QGsrxlxrwBu7ui3NvpfC7rGqJIiUxm7cLrfDbOSHJxutCNOZH90zDZl
xiVBTc3tvpdXdj+zuUkf9Q4dKNTIq3+Hwm3iGXS+yw6rT2C2I3h+IaSCF1YpKakw
MSjOwlmnYIckeYGHe2kfEgNiL7qRD+SxJCjSVusCgYEA2/3Mc5h7K35oQ8tqtl1P
LpyEN22pU6GBhBasqpmOXg/VrPPjkbHHH6tzzEMT97OTaIrg8YqYK1zjm/HmBgHX
ZqTqY2eVNXBJyVseWLlKDrtcFs8ZJZaJDBGrp9/8QdtlGOURwdK/NfaQEHJsJlhn
L6ckSudq8yfyNQJyZf5k+YcCgYEArSPqCBNiICN5Y6YNnDqlWLO3TP8p8Y5rZ9cX
a9SY/W36pWXUiRm3IEN2k3KvhP10DW+zAhqjobh0U2KPHIaSVtmeGNui3eyhNqHU
em7+fq+s1QhTJeo/rQL3bq6mBfxe2Qyi56U6vvmVmXgq8kNOeMb1KyBu3R7suVkC
ui9VPY0CgYEArXpn5sZz8ERHXjeRHTVxaTk2djvgYtzVKjEGR1LzX6Bi7drbw/lF
M1Fjqog/k0tsDM7pC30EoxpRR10hSFdC7dQ+STTeTr1gmB6xJmA8boe1jpAfhFXm
sjjrsFsw3mUpsJD3Ck482T3BA0iZP3NvC/+ge0IkRUC1/j8KP0zQKZI=
-----END RSA PRIVATE KEY-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAv9Dpk+nfW9lOSHdt8UO5f/ELRA+JM4vX9GkbJy9/M/NTv7gg
HBNpxYA/RhIMnifeXFAomLYMov0Gxh4+85Dv4YT3lhxlUiwi2yaOIXlgy15R8kGR
OCFVKgfqfQw3WnWhjGyH3Rb4Znk4IGh2qj+w2lBlnbeKCxXq+SMB/Kc3TMmJg4Cx
P73vVmT3VifW9goOO4DDNAv0uTl/KgWnaVoPc8eRmYxBc9chTxjlmoLHsCy8z9+p
pn33eTyfcNF5DS310sMHLcvpP25fPMpkbzDDjnCzBouI0YkJJ1F2m1GPEqnsN0is
1F9TGJppcAOpb9aaPmtU2wlzMs5Rwm5vQ6/9wQIDAQABAoIBAQCPczl7+QeltRoq
b8a1DCUKXcZDHCtLdWYHzyMTZx4GSA917cl1tb8AiSzIxm7RSJevCfOSYXOJ4RjT
yYLivJ3pVnuis5HCpmda5badqhyNevhl6EsmYydBy7G92wj6icZLMk9ZNPiIClfD
RNyZ7g/g9QdJsB14tOeJcnjl7lgY/8RQOv52mWC2AfvCRGf6fHoopMP4ZlSTDP3q
2LMGNUW+cEwjZg+AIheJCoCOs51pvm/B2DXeb9T/GMzway3qFBKU/RYi+ceOm6UF
+BZwStxpXMRbnXrXv9wS3S6260+NSdgIVF7ErzYjinLhZL9Wc6khoRVOCg2ESewr
+2sJqoABAoGBAOST6/PgAfvODs0m3tzb8dDYMcCtD7p3QoC3HiG7mEu6vnb/GdZZ
6B1XHiK36xLL0/8tf2BtJIqmrDqOJ8pdiT1sbQUtv8lmbdr2PoV2P86v11F8ZBkq
szjpQ6gGaItT3dAjJxSGCiwdvMw4za5GJAUPTQZk+t1XasiaAQznMimBAoGBANbT
9tv+mqwvzK4/Pw/gyIJkIQrbgXTFdMjhZJkNVxCm2R7YnNDRLSxmng0nJCB2Tgkp
EouWNYi9rsWnmR+AOinSSoAb6znQwOZhQzQb2KzkfBnlZ4XFUxvW4OZmJDZILmyr
/8uHnEcT7xwT7L90j9cSxq/+WCbB7GGpFmmVpXRBAoGAd7l/ElsXzuOsXwpoGzjd
HS3QSYKcRWfoHnFLyBFxgOEMmFmgF+U5rfyOnVLGPy8iGHulR0WDqVgJyBXjg5yg
oNqk89x1ozESg2kNcGxymXkDB/xmlcQG4d1UgbLxmWDRQw7Wjmpy846T8Egke47j
mP7dsma7+6mpFe+Mc0y5uoECgYBp1D+u/oz5uA5v5G5Phx+fxG3WqG3stX0jnI1v
LHgwltEs9e7Cm9lSHzdLKXYNm9ozfw1IwHWc6DyZ2EeBkiyU/6h91cMaVzFADLgL
ipBCE8jjBPTrnFqlw0RFnBnIt+RO2qiHfkXJahOH1HTzmBtoCzLf7j9E0JF/Rsno
t7SrQQKBgDxAKKHMLT2zm0kz6eTSLSB7WDNxZ14+u32fZj2ln4JMERTk3byGojPX
zf9poWwCWJOLd7uVtLl2dQZFUZG9GJuZXO15qmKAaXMI5yVwH1ygzzZvDLOlH8Nn
19ZjFhRLreVeAFLMTyOapEH+5QZxaszMR8Xzna9BJnschht/kPM7
-----END RSA PRIVATE KEY-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArmoiX5G36MKPiVGS1sicruEaGRrbhPbIKOf97aGGQRjXVngo
Knwd2L4T9CRyABgQm3tLHHcT5crODoy46wX2g9onTZWViWWuhJ5wxXNmUbCAPWHb
j9SunW53WuLYZ/IJLNZt5XYCAFPjAakWp8uMuuDwWo5EyFaw85X3FSMhVmmaYDd0
cn+1H4+NS/52wX7tWmyvGUNJ8lzjFAnnOtBJByvkyIC7HDphkLQV4j//sMNY1mPX
HbsYgFv2J/LIJtkjdYO2UoDhZG3Gvj16fMy2JE2owA8IX4/s+XAmA2PiTfd0J5b4
drAKEcdDl83G6L3depEkTkfvp0ZLsh9xupAvIwIDAQABAoIBABKGgWonPyKA7+AF
AxS/MC0/CZebC6/+ylnV8lm4K1tkuRKdJp8EmeL4pYPsDxPFepYZLWwzlbB1rxdK
iSWld36fwEb0WXLDkxrQ/Wdrj3Wjyqs6ZqjLTVS5dAH6UEQSKDlT+U5DD4lbX6RA
goCGFUeQNtdXfyTMWHU2+4yKM7NKzUpczFky+0d10Mg0ANj3/4IILdr3hqkmMSI9
1TB9ksWBXJxt3nGxAjzSFihQFUlc231cey/HhYbvAX5fN0xhLxOk88adDcdXE7br
3Ser1q6XaaFQSMj4oi1+h3RAT9MUjJ6johEqjw0PbEZtOqXvA1x5vfFdei6SqgKn
Am3BspkCgYEA2lIiKEkT/Je6ZH4Omhv9atbGoBdETAstL3FnNQjkyVau9f6bxQkl
4/sz985JpaiasORQBiTGY8JDT/hXjROkut91agi2Vafhr29L/mto7KZglfDsT4b2
9z/EZH8wHw7eYhvdoBbMbqNDSI8RrGa4mpLpuN+E0wsFTzSZEL+QMQUCgYEAzIQh
xnreQvDAhNradMqLmxRpayn1ORaPReD4/off+mi7hZRLKtP0iNgEVEWHJ6HEqqi1
r38XAc8ap/lfOVMar2MLyCFOhYspdHZ+TGLZfr8gg/Fzeq9IRGKYadmIKVwjMeyH
REPqg1tyrvMOE0HI5oqkko8JTDJ0OyVC0Vc6+AcCgYAqCzkywugLc/jcU35iZVOH
WLdFq1Vmw5w/D7rNdtoAgCYPj6nV5y4Z2o2mgl6ifXbU7BMRK9Hc8lNeOjg6HfdS
WahV9DmRA1SuIWPkKjE5qczd81i+9AHpmakrpWbSBF4FTNKAewOBpwVVGuBPcDTK
59IE3V7J+cxa9YkotYuCNQKBgCwGla7AbHBEm2z+H+DcaUktD7R+B8gOTzFfyLoi
Tdj+CsAquDO0BQQgXG43uWySql+CifoJhc5h4v8d853HggsXa0XdxaWB256yk2Wm
MePTCRDePVm/ufLetqiyp1kf+IOaw1Oyux0j5oA62mDS3Iikd+EE4Z+BjPvefY/L
E2qpAoGAZo5Wwwk7q8b1n9n/ACh4LpE+QgbFdlJxlfFLJCKstl37atzS8UewOSZj
FDWV28nTP9sqbtsmU8Tem2jzMvZ7C/Q0AuDoKELFUpux8shm8wfIhyaPnXUGZoAZ
Np4vUwMSYV5mopESLWOg3loBxKyLGFtgGKVCjGiQvy6zISQ4fQo=
-----END RSA PRIVATE KEY-----

View file

@ -1,51 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAsj0DJZqC86gp5P0B/KXk4CoWLa06uEpMu4K+0QFNR6XwbB7A
FFXtcjemUnuqnwqHqouG7pcc/KJAiakWPYPbAtwb0JQo3lRPRf6EewjHmspk9vqd
YBqump9VZedCs7acHCT/xoHhsMI4PyTX5VzhTJoZSMSm1aTX91ZTG2R/KkFhchPd
b4kMjc2LK9O1CN8vQoX/IolCOlUOVcVrF20jEiOIFpc+5t4V+jRvx4A52dG0DqBm
Isx3+OioRHD1qRKbi8L851a4UAeX16rW0xsuyCy5htfh3Kbts6AFCtzmvR12QYBl
+aL1Ksj4lraUmQOkwuj6p+w6x2AU3z5Z48qOmUdcmBnSJE9k37wQEqT4Y09O4+Zb
UcaAdcexT1YLEKaL6ghGw9pE7gPTmohk+gTYpiPnQ4H/M7HamN8dHN/Ch1hvaI0/
ZdxSEU2jWlkEeg7c5n7Bmdbc+yMAXeWvS/BeH4yGNh6TtwvTYpq7aDcP4cYw7L7j
ODIWEmCVgOzdDxn5REHmrOx3qSjimCA/Dia2PHSqkO/pJ0Lu/YNHWdEhxJXYtX8x
Ldk7i3syJl+s3Qbi1GrNzPRr+enyAWVk5wM4XVwiNlRvvHh8c+gPs7a5qTS2FulW
psAUB+pCC9Znv8ixgVR9ZMxLALZRIzgZ3s16OZP7pl9t8u87LnbyeihY03sCAwEA
AQKCAgEAm4gqCtI9mykPBcbRyQlqI0IWgF09dDtBog6BPBiKuw7OMUrUCerBfH2b
ITbQuF+T6vo+EEzE+p8K+hUWVy+MGX7Ats3Sq8+eLVHfgQ00QJqEaBBg68/ctQh8
mKOozPF4YAbZOvtzWa7hLhiUXI0j/JgroBgaDSv/WNF3S9vyK4lJ4yX6gK1yyvql
iuT+gHNg5gfPju9/Xy+Bhs7ymEqf4+AljLEGLqd1PhQrxkbaNHyNRoYpGgyaVBWR
X8fCVnrqSJcp4SUHSK6XjZaCR0zdEcgVTNltOgJgQfJM9CG3JydiXd4RHjlY/rDI
W5uPJ8bKK1rp/0ZgNEJfdD8QaXoD28AzoSiL2U8w2wipuq0zAExX9H7ZhgdCMv8n
1yZ3UWhva1LKzjP/fs9ER4CGK7S5U4tmzF8rYSw0A/RuP5bBhGRAJoCs9ZHmM0TU
Z9JDAPLy1/P2ICRuHH6mIVOZkHp0IedSJIPqSJ8LywpPNCyh2lIxksnyL0WnI0ai
8sIu+1t8NGOv/41yUKwJ9LUG+oaWmnUNqduECvYmuX0ByoU/E9YY3RsadiCd5P8l
xviITyRQG+M5BqOTzd7Xa7K9VBy2V7Vnk/gf6bX8fbv/2TYVD4nnjxNd/H6Uzlj5
i1R7/gip4rEA6n/ZbioM8TATaJLG2ZAlnxFAGDw10S3cj1W8gMECggEBANxNwBSx
zbm2vFWCyYY73D+el5cgvSpoXhhu6WotN1aQMBMg4DVQxUSD92erSWvYlb1HOBJL
wjNLMz5NOanRy42mEXso+oN0PMpykGOb1WmoMx186xj7o7p9NK9AFF7ll2DQMYHd
9Y/F8ggmj+4orvMMBL//pYDmdguZ2O+KAr/VZAVj6YTnO/tdmk6n4EhAvB5Hy5wZ
kqi+YD3W01l6vmtWUQBKquaO5XktsAJtv5NapOmy1Fu08gEYwAgADI5Y8CLyMtzX
NmCSoTZ+GNYPIbW4DocSqyH+N6YpyqLDifb3wE7uvyFwYK6y68TNKhO1F7JLQQNM
7pM7F1YkUagzE5kCggEBAM8eYUkXqdno5TTubYcH0KOt7kX2ZB4ABFcYnQsMLAC1
29yElr562jMqgYhpIV+pPMSxKec91g42p6Uiqo1VeciCkFkIcVUyZO1yBFsHnWcS
z1GFDb0ePDAddLIrOmpKQveRpubPThfioqnrrVjm4YObWVwaO/BiAvFF9ZIPgB7N
VmXINKJ75Zof6NsCq4Z27dnnXlh1N2kRDEVV2P/x1HRCq1BB84IePGmSjcHBXRqU
PD0F8PdIYlcW1xDlQuj/x9iWso1MWBWzeuGROTp0LYGA0A6NChU5ldTK3T3+evNo
KO8xzrjbUH6I6XvWitWG1hvHf31UhoiYOuxjF/++zDMCggEAVgSRrELkdc/w516C
u0PiMoEE5YBl/An2O4oK32c6RTVVYBKlGIwqCh+Q2UybBV3y0Y3eSd6EvCxvnLLg
gfslhHBEQRd2AR/AoLdsw0fUY0XGd4wP65hNjIJYsNjPW2I/4hBIVFHLENEUOLR9
3FrMPKADtsfl4leZ3du7RYRYoHh8blJdmoQC+pnIp0+LFgsYqKYVzSR7DCIRR/P6
X+S6NwTj6b49znobBV6ea8RYWfu5inpFymzzVRRJ3pXOUUJOuQZib7IkTD7UbYd8
wQ/1dJOiMIFMiqBNMDb/JOA+nUyNLQSxYigTyAKaZiRJeppp3zbc8qH2QUyARyU1
MPyIeQKCAQA5WO4S8Oxkm6mrKEFHXBCW4XfSA1DhRZvuCbCh+HLOl4wS2NtsTlPQ
SvqmrIVDGXbr9ynlDygPs25juN+EVqBrtksFe+L1dgif/ivakJcyjPC+X5rYPGDp
6Z4AHxwDhiBYsAmIaunyjxv+9HSA4xyZ9g+eAt2Jx3mNGJPQJ16QKMa9U9vPCYMf
U6qDyY94ocFlzjw/PeVjwAanxAdbhrgOoM8SX9BuvLR5fsylU0bWLykmtFht/6rK
9lYCJZiLLxdEjyVNHlBdYd6qSi2QU86txt7UyJR8H/+udaUgny+n6bU71YypfoAh
KQOM+HBkgvsRogFY0GiXtZ7LCP0CIPAlAoIBAG49Ot1AghC9OwLXjcneGRbsML4w
vkQT9vBkStonR56RkCnN8xGDvLwQQXOJ0Fodo8C3Tup5AHrTNi+V69YVQYCtuM4+
heWNZxpYDxzZ1IU5a7ITlH5TJFP5paaaH+tIqRZRp+p2+j5y/CyAe5hoYY4K1Xog
Zuz2piLO5IWKxtznwlQGjlsZ1n6p+WS89NFHuIF2r9NrovPjkEDDqnEcLRTDUnLT
8FcpdlNkarS/X01ZTBFja5EfW49UWW8sQNRLzVfnP+MECvGFy+oA+4vGprSBuxmt
WBY7y+z5VD3o+JVHLqkdETwIP9YKLMVhqUb5DQb2EYwggU+1HgZNqTdbfDk=
-----END RSA PRIVATE KEY-----

View file

@ -1,51 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKgIBAAKCAgEAobIQNPQNtGZlPSpbtZC/c/gb5OxoJfoq07h6X8kyeR3xq9qv
js8u8YQMt/AtgYw1tSdgNeaYzsvMgaSVgexf8ysqys5obWjcB/sjRR6UJY4hgSm5
oLoGy8fi8lbSEtpthHStA7UVUGU95Ypga8r++Kkizk84wDq+0jZAGaEPvhO7RJne
nbEKhhZVIFb13LlkaX1eNOAxAcTszFgX/DaZWFcfzKop6gS7agU5NLCtyAjKdSRz
CXYMVyVnXhZaxZtLawk9Ld8qIqwmytiPwnlRugoFhTrCPIPfAb1UaESKj4EuupUi
brlByun/ENiWvbzTZyGLSuYA6rruyfUll4oDT/SwsDMJ0jZiu5C1bTgA+gY6kB+I
A3EvcP2Eh1uiUvwy5adAr29wlP7/8YwWvyEAAaO9SKbh36xcbsJH9OIQbFati99L
kn7jfyktUGKxgmwYilF2ehtB1tPOMKRor8HJGoMEcrjiu4Qvc3ygaZtomfY4zxL1
uNeZ8c51pm5lmo30kJ5q5aWX6iS4Sp+ncFiPjxj7bV7nXOGqwmxK23tk4qdUOK4s
z26uc+CChP6VdOwLU1uu2FdRvBCPSnvBFIk5qaFZkhbk/gA+iS8R82TUf1DWfvZ4
why6XJDPQRnscySiIckN3X30TD68dpJ5VtzpRzCvNN9GxoRFY6apcJXoV0sCAwEA
AQKCAgAKR01scE8mtpOc7cJiqk7hSlZLmRONxndOeh2dVSbWOCcSq5YZV+Y+CAze
7G+YGpeXamddRclU6/OWEiZG2gXHaWkQ90oAGnhSMY6uaCE2ufA7S7G3G9wuvAgb
K5WzCRuJHfmZkLtIHwduPfufHopSuD20K6kJ3zIeHsC4YFql1I9E7xsNnyFyIJ1M
rvp2C3rskcGZTt8Oo7wByV/M8pOQ4Ajvc6mybJaVSLu4M7r4SkbEZ4rAgTaLm58U
hgtDIHoM1cuDzPnatmLI5jdNP3UIhHaRX4jVW/SjIavp7OF5+dZEmhJUQ4aBJZrH
MV1ztjsiBSnbmv9X7IYdZG39UhKfuqph20l3NQQ9lGhJH68/V5YEt/301O7Jz2jo
hPP56TvvP8sNJZqkiD3JQ9Do4V4s0pUR6RQiim34gdivhRNntPjL1hfBZVoIIxOY
Ek//7OmsfrPwHEatZP6UT+IJTVZRbGp6a5Qu0YmQUh1/MVK+h2PU+FVpkgYALm1u
6e2dXqRK1NI0v0icFMUTBZAw8h7mob7IlkoA/uLWjgkVZf48/2wCiCtxjDiU31Hu
QGV3AZFccjKPwjc+lcWjjH3+nM/q4Z9b2r9x5XaPOlq4mXF4QnheSgQ7URhiEG2I
RI1pmV20HCU/qmAiiGGBWOBYlOSBuRbun5/eSOY8c3FXIMOckQKCAQEAy7F3eqYR
0tXCJRzR9EQld13QVDeP4PpADbMVzuGXbG6+DDjXgRQiDh0gnS2hSKATd61ZRg+m
RHVEPAf9lfEyDutr6qZRZJs14auI0Z4eBfdq+pL23e5tcmj5GuqBZ6qR5ZYHblz+
OT0pEa0VfCW63e0XU/1iG0y6PGbSQiHmqas6E3s7ccDjZZE7lRCBn4+RouBBxtFo
leugrTK+ahObFeSYSQqjG5vGP13gatui5hib8cGG6QUV6GQvezvLvo/rBmCNFrjm
wtQ4vfICifadKYYxyTNNB/EKDrXB1E0NyDM8pmXcfFlajQU0fucvSCNmXIDF8nnL
TCB6mGQBX5VUPwKCAQEAyze5K7YQXe5lp7PLz9IHYyY5dtc4wzwHI4PGe2TAKEtQ
jEOjSi4bgUYGyalJ8vqmOrGkCBW4NCF8Ywb6wNHgK/O6BGbaSl8N2cYqA9wSnVYB
c57ieUCg1A/3mdEwO+jHng1G6Me2Tf7VzkO/XPXyA2Fy6XhIg4xf+mxlW010dbKS
kn4CWljY8XhuX1DZMjA+UAsMzaI6DFcxbIcgcVMP6lp9mnk0Fnm5T5x/EpNjuAHV
D0m9x9TlQ86akFdf+FljEsj/drDIZ8mkU4JTKiH88wrwfKHrOqhv1NX7hLCXs7wb
tkxVF71M2qOcYl5TAvJz9uv2U4O9G5l1jL9wT9eJ9QKCAQEAniUAwGajO+/eNfY0
Q9OMyyo5Dsm8mU1x4bEC44ZejD9GqjKPjpXVAuQ2aBH/QGWX97jMsQqBanEpMvp5
Nar31IGPXbUXSGcA5F7LcQO0B6nakwT7Sb9NliBOF0mugo/5iih7SIJGlqYXdrPN
FIAunxLuo7T8MHnXtgGWiOXNMjnQc0OgGWdKpZamjcss+Hb8+Vnnd7cp3gv8ybu1
/qGOLOc4HK13iX3d42C9VfmEdeTxXjeEyPG72pu+CY2ZWDBgpqjboaKY9vbRvxdg
RUEFMDISAUYlLl9EEbun626Pnrm5Au/eyWSOWyKJaWWQXg+t72/DP8izwD0PMbWj
I1TK/QKCAQEAiYSK5R6OUtIprmPILzlE0H6kclxQSCXN+uWIoiXatynINzLqRB+R
c1is7TiHF0swxBVEGEiCX5ytbOHjPCqKVZPYNHRZkexjFhS4h+YcHqZ90v0Y6s6m
RvsLJebeihwLQVRgwNOs9XjWvH8x9zlj7Y+7UGyaPZL3vCIwMKnofmE6OLHW68am
ADnsDspKQGFPOaFQp7L5LzKt+nAyrx1zbraPusH8Up1Knqobf7mHyJRM1syjBaB3
CPy9saG/CvOKTMMBxRL6eumELxLJLoDTiLDFbsGvygEDtHadfvx1nCZWZnWfO7JZ
WLdQ82w7JoplmRmylm9WwF+HoZhG63DDJQKCAQEAjkYiyHgh/drr0tQLlpELy3+8
3CfKgijIs4Q+UGBsygEkPUzS/oFYl9oJ/yFSSOCsyGTYF9ojXYbdaLyHFS1YcpWN
pcPcZRmd68RdVk2gOMAHBAvT2YN56OQJiDwghMA0bIoY2BI7+h5mFLAgudSZXHY8
RYt9IjEaFai9cvR88p7p9bQJNuIXWXia9PcdSldHJ01S+GhWHNbd6J0BfPhfAO+R
VJl98+U6jLyXyY1Ma1v9AfI6dC43SUKG4Z8b2B0ynxMANiOWXCj823owTk542yFi
ylWbhUGqQ4uMn8ojPArNY+Ndpmlgc8MpgXjaEzMil7BfnhlTHz5sQkMM4ByhaA==
-----END RSA PRIVATE KEY-----

View file

@ -1,51 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAtZndOamd6RPyspdd28050is48R/ozNBdi5xa2023cQ/lWl7j
TFTvYyrp/pJ0GvIIUwG3BKQyQBTKcDhLrQzrTO8fxqKfNSQ9/9J2AVChFulVcaJR
dAJ+k+jjvR+1LMjOxpxjv+RJyTSB4Z6W9xOyqfwAyNsc3nZ2jukHlOCvkpwUJQW2
AMQFLlZoIZ4FyZ5qkSPP8kw6E6bA76/qSY8/bVMleX7HySIAfA7car3lZuxGMkP1
aV7Mak4/5kHZwB75SjuiQVwj3urMA1vEoCuwXgdisMS4lmB8iLyW/XJSWHmLrEnQ
JWbRpie7MYTz2tOJ5GlQpD0U93eV51sn3nQ7If+08IpfZwnsqf+Eew3+cb1scbrI
rwoS2JI9MW97I4iUiLnXl8ZDYy2pIIt3bs5gFIdNVEr29uDnmtvmZfFaj/RuPLO+
4YxNILYo42j8grJyuNikhvCMOWvB/6QMWGUc0BEz4QiZJLzqGWpTP0Nli3AedAxT
6CIbOhfl8RKyZUl4gDD9ZIsXUSZ0dL5CnIYU8yhq9yw45wh8gwPKTo/fiDog44WK
cFiQOlTNg1G6x0fUcbfrsfGwNfc9skXwDz+hcdD7dePZfN7jKeQxtX5dj5DIJAhX
9wanwaoqEj+G5KyXbsn4tuYu2SipSad3uA53UVAHzLapDkD9iiaJ+HrNn3ECAwEA
AQKCAgAW+spgspL13H1YlgjdeIG5k5iYAoat7Cv6L6XbnGD7IJzQK7OthA3qyZJk
kVm50yi0gEINh02IiFj5jFYfJsRbruKhexCUY+qohZRDJFXOFWang3e1K1+jDdRL
qUh+y0ZHIaEJtjSUDl3lE/FcgJSaJ/ZddESZ7fmgqeI4t5nf/noaGTfnruZM78gr
gNiQo8guZ463xWeP9wjxC5ylBEhtaBkU37MeQ3w2NpcztqXhuUJEuA7E76cESLST
SX/pbMH038jvZl5vpdx9DE68Ser+awbVAX+uH7WChALDPYUoBvFistBw+yrKULrC
UGWfKieHzL/UmJofmnVQmltYLfMRarjnGrit2uinJb+xxjt4yKMA6ZCHr6jF7ZkK
F+vKEbLMGZGZcOQvrhMwTkI6asoj3AoDl8pv1AtFlYJi43hlF8fqGHyaEE8KY90M
z5SC0wxhUfUGV4FVHRdq/UvCGQf/bJhRfGftnC1BNc9NkGQHZAbjeu4YSkIjFPEO
k2LwKt8XVFX2bhFNMApP9ktTFlUtOV1Ljjn7+R2L7P/hjRmajnMcSgDZYbQ5Ndw9
fsZ0gTr1NZERgud4CPLgtc6uwp0rYiIwWn2VoMGNM9WUVDhFmOlznKnPqrkw3zH8
uor7/+AI1AGltarvTcth2kriS34AUUoaGysZwIBcYgJSjNPvMQKCAQEA3UVUzjyn
J8UCl+KS5vkXJjIp3mvpT4ZlomSJwuWFChXRHiy033rb2Pz6gCKj8Vh/xHfRSn20
ZlR/hodqhgyZAEkqvvzcBXm6coEwFywZbhN+cJPpqFkrehLBOC+ukOejUo+0Fdm6
pF42hGZngq5GbAi5A60FJO1xOIjPj0chBOk5SUTPz7C8TZnE5ae3iSHEcqB2f7qY
OLLrAAs0YQmOOAB17S6UYcRXTAik6yzONboYFUiyTSid6fuMr9akWP7BGOMNTgdB
RbZB4PYcvMDqJdCOwJV1eqhNz87i3GPAq/D+K+YItxBi4flh8ijrPjpZ4hH22aeL
tmfxZVK5TilW9QKCAQEA0hqZbmnLCj0TV71VZsodb2G+6BRAnS15/ppW1BHuNvmF
noRMnTPAVqbzZ5VZshK0dVsQa66lB6z0uPzOG3euxozrK/Y0a4i0bbdsTO1C+MQ4
ssIasCWTiKaBWuQ+v9yglEvi9nhaQbMUGhwH029xd1e2t7m10O4HYXF1N640j6gu
sLvMc0xNvom/Sv+MP8YoXX0NwwLqqxFVQhPqFsim0EhpfGrxRxmWeby1UB0yxn1N
y87UrF8Ap8MxBImz9avL/5I8LI729xMZywa+RNG3YM9AYsnP2JbZPz3kc2oP0XsL
83lUd8QD+Z2sFB2rL15XnBD1GETOxSvHqjJCcSBBDQKCAQBDp10kqbraGAyQ7//G
i0aesRvIG+p8HDWbD25nntGsobsMpNKwudnaYI8e+nhx5IM8SP4+7mxoFVHgiirx
zYxCYBynxJxpOCzfscxIaX1lAKTaOv9oL8txSaa2TS3stEZlifaf77B3bS7yEHV5
qVty0L/w9cfq4IaLqJj9z9uyqrSPSHDZqcoJWAixxzQAw8hS2+kfaKf+PgZIPyTG
vqszSEDGQkWwFt4yKzpxhYOPPdT7PPz3RoHx9q2vXctmQo4708BPqTw12mIOLHHg
7IMrCLd8/rWqySbxcOpARGe2qrqsJWtovaPeP+fIqOY0Ypb03lVBe07meKWAO2jZ
Ex65AoIBAFDfIDPZ0OeN/sYFALxiC9Z1n1Ahi4V0ncKckdNrW3AZt47+iabw5pX0
CTjTygS7Im8RsE5imO9NaZ1S4dq8xK90SolPaXoC0sBwm+U4ZlDu5owYHsGylQlC
XgQoWubq+3xZgXExfjxPu+sY4wJFoT04rAIoH43eMUUWsPHPwjeRmvc4MkgnFL3E
s7cgilF56suheQyZMM7MCy82DyLZ9Suy07eqSlj9xmfxdTDzLDouvSU35bC7mLr6
bQG8J2Lmz8z98t+L4A/WcFUvsUk4GAfRfo0H9VL/LXwkTK0IJDKT1FPRXewDrSwF
vti3Ws8O11YhSNYglh5a7a3bTqvQqHkCggEBAM/oLCz33rLaNQ7Ogcqd3i08vHrT
CYL+Yxb1ZEIegGgfpIMlYYByhneCcqhXTp8jleJJjRDoaJLfG2rzm4yQ/xIXh2nA
c31BSicJffk+DV1N7BoZG/OC7CSo0GTlJZ8sAKhAfUApYjVHuDp1GZdHBpLmeYs/
zSilstxPBgaKUZ92QNt32gjT/nrq/xYmPDR8AEaurS7cZtkAIsmWEJajmqP9PBNZ
eeCpKaa8m8cSjnSDbTXrA3l7ga2gdEcy++fh+5+VPmpPfnegzImJidC3eBfbDqjn
/zmUYntVTJ6FepsULoaXz1mQGNzgqCx8y3s0TZ4KmwakiRxwS1OUOWSRvtg=
-----END RSA PRIVATE KEY-----

View file

@ -1,51 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKgIBAAKCAgEAoDHNjtZEnbu8o4nWZR8gV4ugYlysC1QP2mxVN66RSXbeuxHi
aYHjMTYxMSaMLBuj240kP5brwmZ/Tpwy6xpDKmfNFebd1HmE38KuWn9fnZQdgylo
w9glcsTLxNOsI/wSV4htF/nRzsvAx95nM7HHbdbHNtPwRhExoNJQ6AOuCVtIAe9B
yAvzdV5voOhWJIahblPbVwU5NX4elm+XGsvlDP66lKcss9lBTjDnWAP4ZcMABBvb
h5Kc5T9aRBz1WAmUP0qs6+b/F/XYPIKzrOfRpWt+I/l60h1a/RkmlMR3xujLBh96
r6ATrYdIHDrg33snCkMUyEe6WfPbD02UTZCIttrfZqWy3ddpmV2FKRiGqPvSHIQa
GBHYZtjV33bdSfmZE25c3LHLCCCP6xJfzK4JMGPEhsDhk3XPWeY3qpKiNAcKSsjg
B8ibgpwCtP8Hb+h1GG3ZLfoMLYmoladL4crnff/RXhab7vrM67Ggz4uyqB+lXbDx
NK3UnmkY6xkX86REMSvJw/tCWBpFxn/2X8UlhgDd7fjgxD7HecowWlXSk+DOdKeD
pDsEGfi2J6/kfRn02QUKnJO6x7gOB5PQPoOjWwQkCaFx4zRngHFXYGuBIv9qNk8W
EtkuSnCbuLnjZrUmSCjgMsxQQqaXo7Cy/UKH97PSaNsD7hXaUmZLd2+kdpcCAwEA
AQKCAgEAi1Ckpxsa015owIToKks2kkxAsCpOCRATNW7PcbxkZ9J0A5abJAysq6io
gUk30Eg9aXvG0XKMGCWRg6j9806EqQVa6zg7JUSFVR/3B4cMfXtJaz8A+IkqkDQr
zkITy7u1q+Bel+JQH5s9TdTSRbfPa2vFFp6csCLV2Tnu2MgSe9qhteUAfVw/X4xA
YlyMRfm7vLo63+QQC8BiE4x6ifhWe8WwOAVnMAW58KlBGF9jkARVKD2d3rqXrhs5
glD44ZZ7Ecv8tK/Qm2LXqlA0uCNnRIhGTDz0HnUfI0vTLL/sNtVPc0S/Kqt5UYl8
IejmlhSBMECEe2U94Grd0OI0HnybF6KtZ1/YDxDS0AQyO6z4CLs8hUKbaLm6V/i4
+9fkWvn4DcS7stTeUe4+I9ua/uaDrrvTNBRH0gaLsl3m3ptPiqN8yigApsCyGrgm
hnxvE1DQCvRw+yeNjgEgtHEJe9z6pcX/0jrw0zH9hIMFM7nSXWONXGQ5AvWuaD1b
cF3JhAP8IYXPqwNDj6dr61vszMqh7iJgQklJm0YbiUiaf00vGpcVKdHYzInIXdoD
rGgPtpoDWx2rAOAdw56IltWV35lSz+zYaPGqQglc+pcMfTqE+7FoFFJmOz2cXxIl
jZehd7dXmBx/yYYedcamLH1A4vmsazJJIxXSRtt2pQ52hdvkU7kCggEBANHr3Ldc
ZYH7KYcowfjfmreAJaRP9SVo+1FtqiwU+I9pzYtT4aXqXLzlJUbHwkWyf3ameghb
EWRKuutC39zz71UsongjVpIPCFZjXXPlo5qxMLAZ0I5KBq4xYEoAplNSJye02HvT
F9KU/J84YwZuJFhWcFXPp5JiFxpW8t1t8iEOuFdvtNkZ1eOByFCC83a/l4sjKCaZ
xemvQCAX9rmZptqAsWAgUmnhJaTcpjv5jfbwqmJDfGgpPxBap8HlpRlF8V0tG/jZ
O0EYoPiHloOD/QoCWaqttQGxxxJW+FfDFo4rlo9xst//6nIVXPU59GbHYBXhio9f
s2UJXOC8sSh9lUsCggEBAMNbouqyltGiX8w3vicEAJDzWubrVkVsRdNyKLh7ARuM
uO1faQ/HuCyoagFx0tt+AnlOQRPU4YprmBZj1f8PSHoCtwfm3FXo7mpJVDu8T5zD
Ja7YOAjmLA42m33ZeAxMdZPTvRYSAP4uxMozrg/tl4md8/lYvStAcoMy5jYvUXF1
E1iWum5EJV4gqkBI5c18QYIzd/hbluxOUhI/fA9AQLK+I56fGMw0hdT48M5Iu/OU
nBVF8tuw7CXNUhxbfIP4Q8ahBcTu3FDBl/qIqsd7Zv7ckaS5Xr7J57189x8zZqNf
k/oHZ0Tdwl5cr29B28YC1vtJ7TLotcDZXXPvOeY/sGUCggEABSkCHPPFfwN4it0C
n6aHfBlHU5mvkgLZoq/Kbhj53zSfm9wtANIZA3+ygeHpMaNopLcE6u2qKMf5fkz/
icPpTzOwrrlXqHF8J/t7UZ0Ef4n5g2qvCMBjF6cZEdigPg4X7k7wv2J6BHArIZLW
RFMyy4Ucb8+R8/Q7UyduAulv+UYOW//f9zI+YsBO90Owzmt5Qy9TDlfbWJo5PlC4
fOl9A4QEWDOTMw0Yysutvm2tArP5zD6ScVEKPtGrrAWEIHHqs/qm5GAap8f+NP3I
QmVdNADIyXxJpcgD97xxkF64UDhcFBycZAs7bSB/T3vkOR6PixonOM0GcOZhBRk+
VZt4rwKCAQEAoal+SwvYpMfi0KM8VxsHwOuxSLBs5uwvaEfrDKa1hu/PxJcU4Psc
HNCNUH65x+sh7vJkBh4/OgXJiJW7a+NgzZ7bic1wfiNQ0GG4M+qkUwxmbab9z9dx
k5161Q0WO8816UvqCI6DhdR8Avv7SbEKmtY8JBZcDKO7X3jKawKDOglxJfktc7wu
1BLh8GqiyIXPzAf9emeIoCo73l/ssM4x+/g+j7AGnE3GhjQvSfWEm5BaDXyh+U0S
TkH3dgH7K1ZR99geZxZm+OkLdEaOVJ943uT2HUNM9UMt42+7LHWjtQSN9vUTbzi3
9NBsWPw9+0E0WCSYBm3uohT+McdAuZnwxQKCAQEAvQhO9GqMMtOnN/QUdU4FHVKl
R8vuJpT3w0ywBcHj5aYwPgkLxCYmfnZtD0kNPkP1FjlYz6C+cGUgNPNifoj6CwCA
oRrw7mgyhHuoum+7qlFwJzhuyx94Z4B7RbINfEsqk4mXGRssUUUBGd4Sh29w54SU
dQMn7s0LiwN6AOwOaAnjD9RlBE+021N9Dax71DuBu1RzA/Z1sC5EFYkL1C5B4kSJ
BYd0Nvru5DRidDrJuxr8tnGqOkNqT2kaujkVmFy6ra6nshkbErbd1LXZksM2PP96
bM5Pta4jX4ylcS8e5F16zmZZrtuBDla1kizM7tGdPQQztXeBHZzvdQDlWTHqkw==
-----END RSA PRIVATE KEY-----

View file

@ -1 +0,0 @@
'|Ę&{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ďĎB­]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

Some files were not shown because too many files have changed in this diff Show more