Skip to content

Commit

Permalink
add client credential flow (#1620)
Browse files Browse the repository at this point in the history
* add client credential flow

Signed-off-by: Noah Kreiger <noahkreiger@gmail.com>

* cleanup test

Signed-off-by: Noah Kreiger <noahkreiger@gmail.com>

* fix license header

Signed-off-by: Noah Kreiger <noahkreiger@gmail.com>

* add license header

Signed-off-by: Noah Kreiger <noahkreiger@gmail.com>

* fix linting issues

Signed-off-by: Noah Kreiger <noahkreiger@gmail.com>

---------

Signed-off-by: Noah Kreiger <noahkreiger@gmail.com>
  • Loading branch information
nkreiger committed Feb 15, 2024
1 parent 8544845 commit 922069a
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 18 deletions.
156 changes: 156 additions & 0 deletions pkg/oauthflow/client_credentials.go
@@ -0,0 +1,156 @@
//
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oauthflow

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)

// CodeURL fetches the client credentials token authorization endpoint URL from the provider's well-known configuration endpoint
func (d *DefaultFlowClientCredentials) CodeURL() (string, error) {
if d.codeURL != "" {
return d.codeURL, nil
}

wellKnown := strings.TrimSuffix(d.Issuer, "/") + "/.well-known/openid-configuration"
/* #nosec */
httpClient := &http.Client{
Timeout: 3 * time.Second,
}
resp, err := httpClient.Get(wellKnown)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("unable to read response body: %w", err)
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("%s: %s", resp.Status, body)
}

providerConfig := struct {
Issuer string `json:"issuer"`
TokenEndpoint string `json:"token_endpoint"`
}{}
if err = json.Unmarshal(body, &providerConfig); err != nil {
return "", fmt.Errorf("oidc: failed to decode provider discovery object: %w", err)
}

if d.Issuer != providerConfig.Issuer {
return "", fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", d.Issuer, providerConfig.Issuer)
}

if providerConfig.TokenEndpoint == "" {
return "", fmt.Errorf("oidc: client credentials token authorization endpoint not returned by provider")
}

d.codeURL = providerConfig.TokenEndpoint
return d.codeURL, nil
}

// DefaultFlowClientCredentials fetches an OIDC Identity token using the Client Credentials Grant flow as specified in RFC8628
type DefaultFlowClientCredentials struct {
Issuer string
codeURL string
}

// NewClientCredentialsFlow creates a new DefaultFlowClientCredentials that retrieves an OIDC Identity Token using a Client Credentials Grant
func NewClientCredentialsFlow(issuer string) *DefaultFlowClientCredentials {
return &DefaultFlowClientCredentials{
Issuer: issuer,
}
}

func (d *DefaultFlowClientCredentials) clientCredentialsFlow(_ *oidc.Provider, clientID, clientSecret, redirectURL string) (string, error) {
data := url.Values{
"client_id": []string{clientID},
"client_secret": []string{clientSecret},
"scope": []string{"openid email"},
"grant_type": []string{"client_credentials"},
}
if redirectURL != "" {
// If a redirect uri is provided then use it
data["redirect_uri"] = []string{redirectURL}
}

codeURL, err := d.CodeURL()
if err != nil {
return "", err
}
/* #nosec */
resp, err := http.PostForm(codeURL, data)
if err != nil {
return "", err
}
defer resp.Body.Close()

b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("%s: %s", resp.Status, b)
}

tr := tokenResp{}
if err := json.Unmarshal(b, &tr); err != nil {
return "", err
}

if tr.IDToken != "" {
fmt.Println("Token received!")
return tr.IDToken, nil
}

return "", fmt.Errorf("unexpected error in client flow: %s", tr.Error)
}

// GetIDToken gets an OIDC ID Token from the specified provider using the Client Credentials Grant flow
func (d *DefaultFlowClientCredentials) GetIDToken(p *oidc.Provider, cfg oauth2.Config) (*OIDCIDToken, error) {
idToken, err := d.clientCredentialsFlow(p, cfg.ClientID, cfg.ClientSecret, cfg.RedirectURL)
if err != nil {
return nil, err
}
verifier := p.Verifier(&oidc.Config{ClientID: cfg.ClientID})
parsedIDToken, err := verifier.Verify(context.Background(), idToken)
if err != nil {
return nil, err
}

subj, err := SubjectFromToken(parsedIDToken)
if err != nil {
return nil, err
}

return &OIDCIDToken{
RawString: idToken,
Subject: subj,
}, nil
}
91 changes: 91 additions & 0 deletions pkg/oauthflow/client_credentials_test.go
@@ -0,0 +1,91 @@
//
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oauthflow

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/coreos/go-oidc/v3/oidc"
)

type testccDriver struct {
respCh chan interface{}
t *testing.T
}

func (td *testccDriver) handler(w http.ResponseWriter, r *http.Request) {
td.t.Log("got request:", r.URL.Path)
if r.URL.Path == "/.well-known/openid-configuration" {
_, _ = w.Write([]byte(strings.ReplaceAll(wellKnownOIDCConfig, "ISSUER", fmt.Sprintf("http://%s", r.Host))))
return
}
nextReply := <-td.respCh
b, err := json.Marshal(nextReply)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

switch r.URL.Path {
case "/token":
_, _ = w.Write(b)
default:
w.WriteHeader(http.StatusBadRequest)
return
}
}

func TestClientCredentialsFlowTokenGetter_ccFlow(t *testing.T) {
td := testccDriver{
respCh: make(chan interface{}, 3),
t: t,
}

ts := httptest.NewServer(http.HandlerFunc(td.handler))
defer ts.Close()

dtg := DefaultFlowClientCredentials{
Issuer: ts.URL,
}
p, pErr := oidc.NewProvider(context.Background(), ts.URL)
if pErr != nil {
t.Fatal(pErr)
}

tokenCh, errCh := make(chan string), make(chan error)
go func() {
token, err := dtg.clientCredentialsFlow(p, "sigstore", "", "")
tokenCh <- token
errCh <- err
}()

td.respCh <- tokenResponse("mytoken", "")

token := <-tokenCh
err := <-errCh
if err != nil {
t.Fatal(err)
}
if token != "mytoken" {
t.Fatal("expected mytoken")
}
}
30 changes: 15 additions & 15 deletions pkg/signature/kms/azure/client_test.go
Expand Up @@ -71,17 +71,17 @@ func (c *testKVClient) Verify(_ context.Context, _, _ string, _ azkeys.VerifyPar

type keyNotFoundClient struct {
testKVClient
key azkeys.JSONWebKey
getKeyReturnsErr bool
key azkeys.JSONWebKey
getKeyReturnsErr bool
getKeyCallThreshold int
getKeyCallCount int
getKeyCallCount int
}

func (c *keyNotFoundClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (azkeys.GetKeyResponse, error) {
if c.getKeyReturnsErr && c.getKeyCallCount < c.getKeyCallThreshold {
c.getKeyCallCount++
return azkeys.GetKeyResponse{}, &azcore.ResponseError{
StatusCode: http.StatusNotFound,
StatusCode: http.StatusNotFound,
RawResponse: &http.Response{},
}
}
Expand All @@ -95,7 +95,7 @@ func (c *keyNotFoundClient) GetKey(_ context.Context, _, _ string, _ *azkeys.Get

type nonResponseErrClient struct {
testKVClient
keyCache *ttlcache.Cache[string, crypto.PublicKey]
keyCache *ttlcache.Cache[string, crypto.PublicKey]
}

func (c *nonResponseErrClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (result azkeys.GetKeyResponse, err error) {
Expand All @@ -105,7 +105,7 @@ func (c *nonResponseErrClient) GetKey(_ context.Context, _, _ string, _ *azkeys.

type non404RespClient struct {
testKVClient
keyCache *ttlcache.Cache[string, crypto.PublicKey]
keyCache *ttlcache.Cache[string, crypto.PublicKey]
}

func (c *non404RespClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (result azkeys.GetKeyResponse, err error) {
Expand Down Expand Up @@ -212,8 +212,8 @@ func TestAzureVaultClientFetchPublicKey(t *testing.T) {

func TestAzureVaultClientCreateKey(t *testing.T) {
type test struct {
name string
client kvClient
name string
client kvClient
expectSuccess bool
}

Expand All @@ -226,27 +226,27 @@ func TestAzureVaultClientCreateKey(t *testing.T) {
{
name: "Successfully create key if it doesn't exist",
client: &keyNotFoundClient{
key: key,
getKeyReturnsErr: true,
key: key,
getKeyReturnsErr: true,
getKeyCallThreshold: 1,
},
expectSuccess: true,
},
{
name: "Return public key if it already exists",
client: &testKVClient{
client: &testKVClient{
key: key,
},
expectSuccess: true,
},
{
name: "Fail to create key due to unknown error",
client: &nonResponseErrClient{},
name: "Fail to create key due to unknown error",
client: &nonResponseErrClient{},
expectSuccess: false,
},
{
name: "Fail to create key due to non-404 status code error",
client: &non404RespClient{},
name: "Fail to create key due to non-404 status code error",
client: &non404RespClient{},
expectSuccess: false,
},
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/signature/kms/azure/integration_test.go
Expand Up @@ -117,7 +117,7 @@ func TestCreateKey(t *testing.T) {
if azureVaultURL == "" {
t.Fatalf("VAULT_URL must be set")
}

newKeyRef := fmt.Sprintf("azurekms://%s.vault.azure.net/%s", azureVaultURL, "new-test-key")

sv, err := LoadSignerVerifier(context.Background(), newKeyRef)
Expand Down
3 changes: 2 additions & 1 deletion test/fuzz/pem/fuzzcert_test.go
Expand Up @@ -18,8 +18,9 @@ package pem
import (
"bytes"
"encoding/pem"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"testing"

"github.com/sigstore/sigstore/pkg/cryptoutils"
)

func FuzzLoadCertificates(f *testing.F) {
Expand Down
2 changes: 1 addition & 1 deletion test/fuzz/signature/fuzz_signature_test.go
Expand Up @@ -33,7 +33,6 @@ import (

func FuzzECDSASigner(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {

x := ecdsa.PrivateKey{}
z := new(big.Int)
z.SetBytes(data)
Expand Down Expand Up @@ -63,6 +62,7 @@ func FuzzECDSASigner(f *testing.F) {
}
})
}

func FuzzComputeDigest(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
hashFuncs := []crypto.Hash{
Expand Down
1 change: 1 addition & 0 deletions test/fuzz/tools.go
@@ -1,3 +1,4 @@
//go:build tools
// +build tools

// Copyright 2021 The Sigstore Authors.
Expand Down

0 comments on commit 922069a

Please sign in to comment.