Skip to content

Commit

Permalink
feat (oidc): add WithVerifier (#141)
Browse files Browse the repository at this point in the history
* feat (oidc): add WithVerifier

This commit adds the WithVerifier option to the
oidc package. This option allows the user to
specify a custom verifier for the PKCE code
challenge.

* chore (changelog): add PR #141 to changelog
  • Loading branch information
jimlambrt authored Oct 22, 2024
1 parent b85f9a7 commit 67f22c4
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 8 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@

Canonical reference for changes, improvements, and bugfixes for cap.

## Next

* feat (oidc): add WithVerifier ([PR #141](https://github.com/hashicorp/cap/pull/141))

## 0.7.0

* Add ability to the SAML test provider to create signed SAML responses by
@hcjulz ([PR: 135](https://github.com/hashicorp/cap/pull/135))
* Bump golang.org/x/net from 0.22.0 to 0.23.0 by @dependabot ([PR #136](https://github.com/hashicorp/cap/pull/136))
* feat (config): add support for a http.RoundTripper by @jimlambrt ([PR #137](https://github.com/hashicorp/cap/pull/137))
* chore: update deps by @jimlambrt ([PR #138](https://github.com/hashicorp/cap/pull/138))

## 0.6.0

* Add case insensitive user attribute keys configs for LDAP by @jasonodonnell in https://github.com/hashicorp/cap/pull/132
* chore (oidc, jwt, ldap): update deps by @jimlambrt in https://github.com/hashicorp/cap/pull/133
* chore (oidc, jwt, ldap): update deps by @jimlambrt in **https**://github.com/hashicorp/cap/pull/133
* Add empty anonymous group search configs by @jasonodonnell in https://github.com/hashicorp/cap/pull/134

## 0.5.0
Expand Down
84 changes: 78 additions & 6 deletions oidc/pkce_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"regexp"

"github.com/hashicorp/cap/oidc/internal/base62"
)
Expand Down Expand Up @@ -51,19 +52,39 @@ type S256Verifier struct {
}

// min len of 43 chars per https://tools.ietf.org/html/rfc7636#section-4.1
const verifierLen = 43
const (
// min len of 43 chars per https://tools.ietf.org/html/rfc7636#section-4.1
minVerifierLen = 43
// max len of 128 chars per https://tools.ietf.org/html/rfc7636#section-4.1
maxVerifierLen = 128
)

// NewCodeVerifier creates a new CodeVerifier (*S256Verifier).
// Supported options: WithVerifier
//
// See: https://tools.ietf.org/html/rfc7636#section-4.1
func NewCodeVerifier() (*S256Verifier, error) {
func NewCodeVerifier(opt ...Option) (*S256Verifier, error) {
const op = "NewCodeVerifier"
data, err := base62.Random(verifierLen)
if err != nil {
return nil, fmt.Errorf("%s: unable to create verifier data %w", op, err)
var (
err error
verifierData string
)
opts := getPKCEOpts(opt...)
switch {
case opts.withVerifier != "":
verifierData = opts.withVerifier
default:
var err error
verifierData, err = base62.Random(minVerifierLen)
if err != nil {
return nil, fmt.Errorf("%s: unable to create verifier data %w", op, err)
}
}
if err := verifierIsValid(verifierData); err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
v := &S256Verifier{
verifier: data, // no need to encode it, since bas62.Random uses a limited set of characters.
verifier: verifierData, // no need to encode it, since bas62.Random uses a limited set of characters.
method: S256,
}
if v.challenge, err = CreateCodeChallenge(v); err != nil {
Expand All @@ -72,6 +93,25 @@ func NewCodeVerifier() (*S256Verifier, error) {
return v, nil
}

func verifierIsValid(v string) error {
const op = "verifierIsValid"
switch {
case len(v) < minVerifierLen:
return fmt.Errorf("%s: verifier length is less than %d", op, minVerifierLen)
case len(v) > maxVerifierLen:
return fmt.Errorf("%s: verifier length is greater than %d", op, maxVerifierLen)
default:
// check that the verifier is valid based on
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
// Check for valid characters: A-Z, a-z, 0-9, -, _, ., ~
validChars := regexp.MustCompile(`^[A-Za-z0-9\-\._~]+$`)
if !validChars.MatchString(v) {
return fmt.Errorf("%s: verifier contains invalid characters", op)
}
}
return nil
}

func (v *S256Verifier) Verifier() string { return v.verifier } // Verifier implements the CodeVerifier.Verifier() interface function.
func (v *S256Verifier) Challenge() string { return v.challenge } // Challenge implements the CodeVerifier.Challenge() interface function.
func (v *S256Verifier) Method() ChallengeMethod { return v.method } // Method implements the CodeVerifier.Method() interface function.
Expand Down Expand Up @@ -99,3 +139,35 @@ func CreateCodeChallenge(v CodeVerifier) (string, error) {
sum := h.Sum(nil)
return base64.RawURLEncoding.EncodeToString(sum), nil
}

// pkceOptions is the set of available options.
type pkceOptions struct {
withVerifier string
}

// pkceDefaults is a handy way to get the defaults at runtime and
// during unit tests.
func pkceDefaults() pkceOptions {
return pkceOptions{}
}

// getPKCEOpts gets the defaults and applies the opt overrides passed in.
func getPKCEOpts(opt ...Option) pkceOptions {
opts := pkceDefaults()
ApplyOpts(&opts, opt...)
return opts
}

// WithVerifier provides an optional verifier for the code verifier. When this
// option is provided, NewCodeVerifier will use the provided verifier. Note the
// verifier must use the base62 character set.
// See: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
//
// Valid for: NewVerifier
func WithVerifier(verifier string) Option {
return func(o interface{}) {
if o, ok := o.(*pkceOptions); ok {
o.withVerifier = verifier
}
}
}
35 changes: 34 additions & 1 deletion oidc/pkce_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"testing"

"github.com/hashicorp/cap/oidc/internal/base62"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -18,7 +19,7 @@ func TestNewCodeVerifier(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := NewCodeVerifier()
require.NoError(err)
assert.Equal(verifierLen, len(got.verifier))
assert.Equal(minVerifierLen, len(got.verifier))
assert.Equal(S256, got.Method())

challenge, err := CreateCodeChallenge(got)
Expand Down Expand Up @@ -53,3 +54,35 @@ func TestCreateCodeChallenge(t *testing.T) {
assert.True(errors.Is(err, ErrUnsupportedChallengeMethod))
})
}

func Test_WithVerifier(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
v, err := base62.Random(43)
require.NoError(err)
got, err := NewCodeVerifier(WithVerifier(v))
require.NoError(err)
assert.Equal(v, got.Verifier())

// Test that the verifier is too short
v, err = base62.Random(42)
require.NoError(err)
_, err = NewCodeVerifier(WithVerifier(v))
require.Error(err)
assert.Contains(err.Error(), "verifier length is less than 43")

// Test that the verifier is too long
v, err = base62.Random(129)
require.NoError(err)
_, err = NewCodeVerifier(WithVerifier(v))
require.Error(err)
assert.Contains(err.Error(), "verifier length is greater than 128")

// Test that the verifier contains invalid characters
v, err = base62.Random(43)
require.NoError(err)
v = v + "!"
_, err = NewCodeVerifier(WithVerifier(v))
require.Error(err)
assert.Contains(err.Error(), "verifier contains invalid characters")
}

0 comments on commit 67f22c4

Please sign in to comment.