diff -Nru golang-github-coreos-go-oidc-2.0.0/debian/changelog golang-github-coreos-go-oidc-2.1.0/debian/changelog --- golang-github-coreos-go-oidc-2.0.0/debian/changelog 2019-10-20 13:34:47.000000000 +0000 +++ golang-github-coreos-go-oidc-2.1.0/debian/changelog 2019-12-16 00:37:19.000000000 +0000 @@ -1,3 +1,9 @@ +golang-github-coreos-go-oidc (2.1.0-1) unstable; urgency=medium + + * New upstream release. + + -- Dmitry Smirnov Mon, 16 Dec 2019 11:37:19 +1100 + golang-github-coreos-go-oidc (2.0.0-1) unstable; urgency=medium [ Alexandre Viau ] diff -Nru golang-github-coreos-go-oidc-2.0.0/MAINTAINERS golang-github-coreos-go-oidc-2.1.0/MAINTAINERS --- golang-github-coreos-go-oidc-2.0.0/MAINTAINERS 2018-04-17 20:27:37.000000000 +0000 +++ golang-github-coreos-go-oidc-2.1.0/MAINTAINERS 2019-08-15 17:57:29.000000000 +0000 @@ -1,2 +1,3 @@ -Eric Chiang (@ericchiang) +Eric Chiang (@ericchiang) +Mike Danese (@mikedanese) Rithu Leena John (@rithujohn191) diff -Nru golang-github-coreos-go-oidc-2.0.0/oidc.go golang-github-coreos-go-oidc-2.1.0/oidc.go --- golang-github-coreos-go-oidc-2.0.0/oidc.go 2018-04-17 20:27:37.000000000 +0000 +++ golang-github-coreos-go-oidc-2.1.0/oidc.go 2019-08-15 17:57:29.000000000 +0000 @@ -261,6 +261,9 @@ // Raw payload of the id_token. claims []byte + + // Map of distributed claim names to claim sources + distributedClaims map[string]claimSource } // Claims unmarshals the raw JSON payload of the ID Token into a provided struct. @@ -313,13 +316,21 @@ } type idToken struct { - Issuer string `json:"iss"` - Subject string `json:"sub"` - Audience audience `json:"aud"` - Expiry jsonTime `json:"exp"` - IssuedAt jsonTime `json:"iat"` - Nonce string `json:"nonce"` - AtHash string `json:"at_hash"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience audience `json:"aud"` + Expiry jsonTime `json:"exp"` + IssuedAt jsonTime `json:"iat"` + NotBefore *jsonTime `json:"nbf"` + Nonce string `json:"nonce"` + AtHash string `json:"at_hash"` + ClaimNames map[string]string `json:"_claim_names"` + ClaimSources map[string]claimSource `json:"_claim_sources"` +} + +type claimSource struct { + Endpoint string `json:"endpoint"` + AccessToken string `json:"access_token"` } type audience []string diff -Nru golang-github-coreos-go-oidc-2.0.0/.travis.yml golang-github-coreos-go-oidc-2.1.0/.travis.yml --- golang-github-coreos-go-oidc-2.0.0/.travis.yml 2018-04-17 20:27:37.000000000 +0000 +++ golang-github-coreos-go-oidc-2.1.0/.travis.yml 2019-08-15 17:57:29.000000000 +0000 @@ -1,8 +1,8 @@ language: go go: - - 1.7.5 - - 1.8 + - "1.9" + - "1.10" install: - go get -v -t github.com/coreos/go-oidc/... diff -Nru golang-github-coreos-go-oidc-2.0.0/verify.go golang-github-coreos-go-oidc-2.1.0/verify.go --- golang-github-coreos-go-oidc-2.0.0/verify.go 2018-04-17 20:27:37.000000000 +0000 +++ golang-github-coreos-go-oidc-2.1.0/verify.go 2019-08-15 17:57:29.000000000 +0000 @@ -7,6 +7,8 @@ "encoding/json" "errors" "fmt" + "io/ioutil" + "net/http" "strings" "time" @@ -85,6 +87,15 @@ // If true, token expiry is not checked. SkipExpiryCheck bool + // SkipIssuerCheck is intended for specialized cases where the the caller wishes to + // defer issuer validation. When enabled, callers MUST independently verify the Token's + // Issuer is a known good value. + // + // Mismatched issuers often indicate client mis-configuration. If mismatches are + // unexpected, evaluate if the provided issuer URL is incorrect instead of enabling + // this option. + SkipIssuerCheck bool + // Time function to check Token expiry. Defaults to time.Now Now func() time.Time } @@ -118,6 +129,53 @@ return false } +// Returns the Claims from the distributed JWT token +func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) { + req, err := http.NewRequest("GET", src.Endpoint, nil) + if err != nil { + return nil, fmt.Errorf("malformed request: %v", err) + } + if src.AccessToken != "" { + req.Header.Set("Authorization", "Bearer "+src.AccessToken) + } + + resp, err := doRequest(ctx, req) + if err != nil { + return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode) + } + + token, err := verifier.Verify(ctx, string(body)) + if err != nil { + return nil, fmt.Errorf("malformed response body: %v", err) + } + + return token.claims, nil +} + +func parseClaim(raw []byte, name string, v interface{}) error { + var parsed map[string]json.RawMessage + if err := json.Unmarshal(raw, &parsed); err != nil { + return err + } + + val, ok := parsed[name] + if !ok { + return fmt.Errorf("claim doesn't exist: %s", name) + } + + return json.Unmarshal([]byte(val), v) +} + // Verify parses a raw ID Token, verifies it's been signed by the provider, preforms // any additional checks depending on the Config, and returns the payload. // @@ -155,19 +213,34 @@ return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) } + distributedClaims := make(map[string]claimSource) + + //step through the token to map claim names to claim sources" + for cn, src := range token.ClaimNames { + if src == "" { + return nil, fmt.Errorf("oidc: failed to obtain source from claim name") + } + s, ok := token.ClaimSources[src] + if !ok { + return nil, fmt.Errorf("oidc: source does not exist") + } + distributedClaims[cn] = s + } + t := &IDToken{ - Issuer: token.Issuer, - Subject: token.Subject, - Audience: []string(token.Audience), - Expiry: time.Time(token.Expiry), - IssuedAt: time.Time(token.IssuedAt), - Nonce: token.Nonce, - AccessTokenHash: token.AtHash, - claims: payload, + Issuer: token.Issuer, + Subject: token.Subject, + Audience: []string(token.Audience), + Expiry: time.Time(token.Expiry), + IssuedAt: time.Time(token.IssuedAt), + Nonce: token.Nonce, + AccessTokenHash: token.AtHash, + claims: payload, + distributedClaims: distributedClaims, } // Check issuer. - if t.Issuer != v.issuer { + if !v.config.SkipIssuerCheck && t.Issuer != v.issuer { // Google sometimes returns "accounts.google.com" as the issuer claim instead of // the required "https://accounts.google.com". Detect this case and allow it only // for Google. @@ -197,10 +270,21 @@ if v.config.Now != nil { now = v.config.Now } + nowTime := now() - if t.Expiry.Before(now()) { + if t.Expiry.Before(nowTime) { return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry) } + + // If nbf claim is provided in token, ensure that it is indeed in the past. + if token.NotBefore != nil { + nbfTime := time.Time(*token.NotBefore) + leeway := 1 * time.Minute + + if nowTime.Add(leeway).Before(nbfTime) { + return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime) + } + } } switch len(jws.Signatures) { diff -Nru golang-github-coreos-go-oidc-2.0.0/verify_test.go golang-github-coreos-go-oidc-2.1.0/verify_test.go --- golang-github-coreos-go-oidc-2.0.0/verify_test.go 2018-04-17 20:27:37.000000000 +0000 +++ golang-github-coreos-go-oidc-2.1.0/verify_test.go 2019-08-15 17:57:29.000000000 +0000 @@ -3,6 +3,10 @@ import ( "context" "fmt" + "io" + "net/http" + "net/http/httptest" + "reflect" "strconv" "testing" "time" @@ -45,6 +49,17 @@ wantErr: true, }, { + name: "skip issuer check", + issuer: "https://bar", + idToken: `{"iss":"https://foo"}`, + config: Config{ + SkipIssuerCheck: true, + SkipClientIDCheck: true, + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + }, + { name: "invalid sig", idToken: `{"iss":"https://foo"}`, config: Config{ @@ -92,6 +107,34 @@ }, signKey: newRSAKey(t), }, + { + name: "nbf in future", + idToken: `{"iss":"https://foo","nbf":` + strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10) + + `,"exp":` + strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10) + `}`, + config: Config{ + SkipClientIDCheck: true, + }, + signKey: newRSAKey(t), + wantErr: true, + }, + { + name: "nbf in past", + idToken: `{"iss":"https://foo","nbf":` + strconv.FormatInt(time.Now().Add(-time.Hour).Unix(), 10) + + `,"exp":` + strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10) + `}`, + config: Config{ + SkipClientIDCheck: true, + }, + signKey: newRSAKey(t), + }, + { + name: "nbf in future within clock skew tolerance", + idToken: `{"iss":"https://foo","nbf":` + strconv.FormatInt(time.Now().Add(30*time.Second).Unix(), 10) + + `,"exp":` + strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10) + `}`, + config: Config{ + SkipClientIDCheck: true, + }, + signKey: newRSAKey(t), + }, } for _, test := range tests { t.Run(test.name, test.run) @@ -204,16 +247,285 @@ signKey: newRSAKey(t), } t.Run("at_hash", func(t *testing.T) { - tok := vt.runGetToken(t) - if tok != nil { - if tok.AccessTokenHash != atHash { - t.Errorf("access token hash not preserved correctly, want %q got %q", atHash, tok.AccessTokenHash) + tok, err := vt.runGetToken(t) + if err != nil { + t.Errorf("parsing token: %v", err) + return + } + if tok.AccessTokenHash != atHash { + t.Errorf("access token hash not preserved correctly, want %q got %q", atHash, tok.AccessTokenHash) + } + if tok.sigAlgorithm != RS256 { + t.Errorf("invalid signature algo, want %q got %q", RS256, tok.sigAlgorithm) + } + }) +} + +func TestDistributedClaims(t *testing.T) { + tests := []struct { + test verificationTest + want map[string]claimSource + wantErr bool + }{ + { + test: verificationTest{ + name: "NoDistClaims", + idToken: `{"iss":"https://foo","aud":"client1"}`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + }, + want: map[string]claimSource{}, + }, + { + test: verificationTest{ + name: "1DistClaim", + idToken: `{ + "iss":"https://foo","aud":"client1", + "_claim_names": { + "address": "src1" + }, + "_claim_sources": { + "src1": {"endpoint": "123", "access_token":"1234"} + } + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + }, + want: map[string]claimSource{ + "address": claimSource{Endpoint: "123", AccessToken: "1234"}, + }, + }, + { + test: verificationTest{ + name: "2DistClaims1Src", + idToken: `{ + "iss":"https://foo","aud":"client1", + "_claim_names": { + "address": "src1", + "phone_number": "src1" + }, + "_claim_sources": { + "src1": {"endpoint": "123", "access_token":"1234"} + } + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + }, + want: map[string]claimSource{ + "address": claimSource{Endpoint: "123", AccessToken: "1234"}, + "phone_number": claimSource{Endpoint: "123", AccessToken: "1234"}, + }, + }, + { + test: verificationTest{ + name: "1Name0Src", + idToken: `{ + "iss":"https://foo","aud":"client1", + "_claim_names": { + "address": "src1" + }, + "_claim_sources": { + } + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + }, + wantErr: true, + }, + { + test: verificationTest{ + name: "NoNames1Src", + idToken: `{ + "iss":"https://foo","aud":"client1", + "_claim_names": { + }, + "_claim_sources": { + "src1": {"endpoint": "https://foo", "access_token":"1234"} + } + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + }, + want: map[string]claimSource{}, + }, + } + for _, test := range tests { + t.Run(test.test.name, func(t *testing.T) { + idToken, err := test.test.runGetToken(t) + if err != nil { + if !test.wantErr { + t.Errorf("parsing token: %v", err) + } + return + } + if test.wantErr { + t.Errorf("expected error parsing token") + return + } + if !reflect.DeepEqual(idToken.distributedClaims, test.want) { + t.Errorf("expected distributed claim: %#v, got: %#v", test.want, idToken.distributedClaims) + } + }) + } +} + +func TestDistClaimResolver(t *testing.T) { + tests := []resolverTest{ + { + name: "noAccessToken", + payload: `{"iss":"https://foo","aud":"client1", + "email":"janedoe@email.com", + "shipping_address": { + "street_address": "1234 Hollywood Blvd.", + "locality": "Los Angeles", + "region": "CA", + "postal_code": "90210", + "country": "US"} + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + issuer: "https://foo", + + want: map[string]claimSource{}, + }, + { + name: "rightAccessToken", + payload: `{"iss":"https://foo","aud":"client1", + "email":"janedoe@email.com", + "shipping_address": { + "street_address": "1234 Hollywood Blvd.", + "locality": "Los Angeles", + "region": "CA", + "postal_code": "90210", + "country": "US"} + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + accessToken: "1234", + issuer: "https://foo", + + want: map[string]claimSource{}, + }, + { + name: "wrongAccessToken", + payload: `{"iss":"https://foo","aud":"client1", + "email":"janedoe@email.com", + "shipping_address": { + "street_address": "1234 Hollywood Blvd.", + "locality": "Los Angeles", + "region": "CA", + "postal_code": "90210", + "country": "US"} + }`, + config: Config{ + ClientID: "client1", + SkipExpiryCheck: true, + }, + signKey: newRSAKey(t), + accessToken: "12345", + issuer: "https://foo", + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + claims, err := test.testEndpoint(t) + if err != nil { + if !test.wantErr { + t.Errorf("%v", err) + } + return + } + if test.wantErr { + t.Errorf("expected error receiving response") + return } - if tok.sigAlgorithm != RS256 { - t.Errorf("invalid signature algo, want %q got %q", RS256, tok.sigAlgorithm) + if !reflect.DeepEqual(string(claims), test.payload) { + t.Errorf("expected dist claim: %#v, got: %v", test.payload, string(claims)) } + }) + } + +} + +type resolverTest struct { + // Name of the subtest. + name string + + // issuer will be the endpoint server url + issuer string + + // just the payload + payload string + + // Key to sign the ID Token with. + signKey *signingKey + + // If not provided defaults to signKey. Only useful when + // testing invalid signatures. + verificationKey *signingKey + + config Config + wantErr bool + want map[string]claimSource + + //this is the access token that the testEndpoint will accept + accessToken string +} + +func (v resolverTest) testEndpoint(t *testing.T) ([]byte, error) { + token := v.signKey.sign(t, []byte(v.payload)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got := r.Header.Get("Authorization") + if got != "" && got != "Bearer 1234" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return } - }) + io.WriteString(w, token) + })) + defer s.Close() + + issuer := v.issuer + var ks KeySet + if v.verificationKey == nil { + ks = &testVerifier{v.signKey.jwk()} + } else { + ks = &testVerifier{v.verificationKey.jwk()} + } + verifier := NewVerifier(issuer, ks, &v.config) + + ctx = ClientContext(ctx, s.Client()) + + src := claimSource{ + Endpoint: s.URL + "/", + AccessToken: v.accessToken, + } + return resolveDistributedClaim(ctx, verifier, src) } type verificationTest struct { @@ -236,7 +548,7 @@ wantErr bool } -func (v verificationTest) runGetToken(t *testing.T) *IDToken { +func (v verificationTest) runGetToken(t *testing.T) (*IDToken, error) { token := v.signKey.sign(t, []byte(v.idToken)) ctx, cancel := context.WithCancel(context.Background()) @@ -254,19 +566,15 @@ } verifier := NewVerifier(issuer, ks, &v.config) - idToken, err := verifier.Verify(ctx, token) - if err != nil { - if !v.wantErr { - t.Errorf("%s: verify %v", v.name, err) - } - } else { - if v.wantErr { - t.Errorf("%s: expected error", v.name) - } - } - return idToken + return verifier.Verify(ctx, token) } func (v verificationTest) run(t *testing.T) { - v.runGetToken(t) + _, err := v.runGetToken(t) + if err != nil && !v.wantErr { + t.Errorf("%v", err) + } + if err == nil && v.wantErr { + t.Errorf("expected error") + } }