proxy: add userinfo and webauthn endpoints (#3755)

* proxy: add userinfo and webauthn endpoints

* use TLD for RP id

* use EffectiveTLDPlusOne

* upgrade webauthn

* fix test

* Update internal/handlers/jwks.go

Co-authored-by: bobby <1544881+desimone@users.noreply.github.com>

Co-authored-by: bobby <1544881+desimone@users.noreply.github.com>
This commit is contained in:
Caleb Doxsey 2022-11-22 10:26:35 -07:00 committed by GitHub
parent 81053ac8ef
commit c1a522cd82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 498 additions and 216 deletions

View file

@ -3,6 +3,7 @@ package webauthnutil
import (
"encoding/base64"
"fmt"
"net/http"
"time"
"github.com/pomerium/webauthn"
@ -25,12 +26,14 @@ func GenerateChallenge(key []byte, expiry time.Time) cryptutil.SecureToken {
// GenerateCreationOptions generates creation options for WebAuthn.
func GenerateCreationOptions(
r *http.Request,
key []byte,
deviceType *device.Type,
user *user.User,
) *webauthn.PublicKeyCredentialCreationOptions {
expiry := time.Now().Add(ceremonyTimeout)
return newCreationOptions(
r,
GenerateChallenge(key, expiry).Bytes(),
deviceType,
user,
@ -39,12 +42,14 @@ func GenerateCreationOptions(
// GenerateRequestOptions generates request options for WebAuthn.
func GenerateRequestOptions(
r *http.Request,
key []byte,
deviceType *device.Type,
knownDeviceCredentials []*device.Credential,
) *webauthn.PublicKeyCredentialRequestOptions {
expiry := time.Now().Add(ceremonyTimeout)
return newRequestOptions(
r,
GenerateChallenge(key, expiry).Bytes(),
deviceType,
knownDeviceCredentials,
@ -54,6 +59,7 @@ func GenerateRequestOptions(
// GetCreationOptionsForCredential gets the creation options for the public key creation credential. An error may be
// returned if the challenge used to generate the credential is invalid.
func GetCreationOptionsForCredential(
r *http.Request,
key []byte,
deviceType *device.Type,
user *user.User,
@ -76,12 +82,13 @@ func GetCreationOptionsForCredential(
return nil, err
}
return newCreationOptions(challenge.Bytes(), deviceType, user), nil
return newCreationOptions(r, challenge.Bytes(), deviceType, user), nil
}
// GetRequestOptionsForCredential gets the request options for the public key request credential. An error may be
// returned if the challenge used to generate the credential is invalid.
func GetRequestOptionsForCredential(
r *http.Request,
key []byte,
deviceType *device.Type,
knownDeviceCredentials []*device.Credential,
@ -104,11 +111,12 @@ func GetRequestOptionsForCredential(
return nil, err
}
return newRequestOptions(challenge.Bytes(), deviceType, knownDeviceCredentials), nil
return newRequestOptions(r, challenge.Bytes(), deviceType, knownDeviceCredentials), nil
}
// newCreationOptions gets the creation options for WebAuthn with the provided challenge.
func newCreationOptions(
r *http.Request,
challenge []byte,
deviceType *device.Type,
user *user.User,
@ -116,6 +124,7 @@ func newCreationOptions(
options := &webauthn.PublicKeyCredentialCreationOptions{
RP: webauthn.PublicKeyCredentialRPEntity{
Name: rpName,
ID: GetEffectiveDomain(r),
},
User: GetUserEntity(user),
Challenge: challenge,
@ -133,6 +142,7 @@ func newCreationOptions(
// newRequestOptions gets the request options for WebAuthn with the provided challenge.
func newRequestOptions(
r *http.Request,
challenge []byte,
deviceType *device.Type,
knownDeviceCredentials []*device.Credential,
@ -140,6 +150,7 @@ func newRequestOptions(
options := &webauthn.PublicKeyCredentialRequestOptions{
Challenge: challenge,
Timeout: ceremonyTimeout,
RPID: GetEffectiveDomain(r),
}
fillRequestUserVerificationRequirement(
options,

View file

@ -1,9 +1,11 @@
package webauthnutil
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/pkg/grpc/device"
"github.com/pomerium/pomerium/pkg/grpc/user"
@ -12,14 +14,17 @@ import (
)
func TestGenerateCreationOptions(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "https://www.example.com", nil)
require.NoError(t, err)
t.Run("random challenge", func(t *testing.T) {
key := []byte{1, 2, 3}
options1 := GenerateCreationOptions(key, predefinedDeviceTypes[DefaultDeviceType], &user.User{
options1 := GenerateCreationOptions(r, key, predefinedDeviceTypes[DefaultDeviceType], &user.User{
Id: "example",
Email: "test@example.com",
Name: "Test User",
})
options2 := GenerateCreationOptions(key, predefinedDeviceTypes[DefaultDeviceType], &user.User{
options2 := GenerateCreationOptions(r, key, predefinedDeviceTypes[DefaultDeviceType], &user.User{
Id: "example",
Email: "test@example.com",
Name: "Test User",
@ -28,7 +33,7 @@ func TestGenerateCreationOptions(t *testing.T) {
})
t.Run(DefaultDeviceType, func(t *testing.T) {
key := []byte{1, 2, 3}
options := GenerateCreationOptions(key, predefinedDeviceTypes[DefaultDeviceType], &user.User{
options := GenerateCreationOptions(r, key, predefinedDeviceTypes[DefaultDeviceType], &user.User{
Id: "example",
Email: "test@example.com",
Name: "Test User",
@ -37,6 +42,7 @@ func TestGenerateCreationOptions(t *testing.T) {
assert.Equal(t, &webauthn.PublicKeyCredentialCreationOptions{
RP: webauthn.PublicKeyCredentialRPEntity{
Name: "Pomerium",
ID: "example.com",
},
User: webauthn.PublicKeyCredentialUserEntity{
ID: []byte{
@ -63,15 +69,18 @@ func TestGenerateCreationOptions(t *testing.T) {
}
func TestGenerateRequestOptions(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "https://www.example.com", nil)
require.NoError(t, err)
t.Run("random challenge", func(t *testing.T) {
key := []byte{1, 2, 3}
options1 := GenerateRequestOptions(key, predefinedDeviceTypes[DefaultDeviceType], nil)
options2 := GenerateRequestOptions(key, predefinedDeviceTypes[DefaultDeviceType], nil)
options1 := GenerateRequestOptions(r, key, predefinedDeviceTypes[DefaultDeviceType], nil)
options2 := GenerateRequestOptions(r, key, predefinedDeviceTypes[DefaultDeviceType], nil)
assert.NotEqual(t, options1.Challenge, options2.Challenge)
})
t.Run(DefaultDeviceType, func(t *testing.T) {
key := []byte{1, 2, 3}
options := GenerateRequestOptions(key, predefinedDeviceTypes[DefaultDeviceType], []*device.Credential{
options := GenerateRequestOptions(r, key, predefinedDeviceTypes[DefaultDeviceType], []*device.Credential{
{Id: "device1", Specifier: &device.Credential_Webauthn{Webauthn: &device.Credential_WebAuthn{
Id: []byte{4, 5, 6},
}}},
@ -79,6 +88,7 @@ func TestGenerateRequestOptions(t *testing.T) {
options.Challenge = nil
assert.Equal(t, &webauthn.PublicKeyCredentialRequestOptions{
Timeout: 900000000000,
RPID: "example.com",
AllowCredentials: []webauthn.PublicKeyCredentialDescriptor{
{Type: "public-key", ID: []byte{4, 5, 6}},
},
@ -129,7 +139,8 @@ func TestFillPublicKeyCredentialParameters(t *testing.T) {
}{
{"", 0, nil},
{"public-key", -7, &device.WebAuthnOptions_PublicKeyCredentialParameters{
Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: -7}},
Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: -7,
}},
} {
params := new(webauthn.PublicKeyCredentialParameters)
fillPublicKeyCredentialParameters(params, testCase.in)

View file

@ -1,2 +1,32 @@
// Package webauthnutil contains types and functions for working with the webauthn package.
package webauthnutil
import (
"net"
"net/http"
"golang.org/x/net/publicsuffix"
"github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/webauthn"
)
// GetRelyingParty gets a RelyingParty for the given request and databroker client.
func GetRelyingParty(r *http.Request, client databroker.DataBrokerServiceClient) *webauthn.RelyingParty {
return webauthn.NewRelyingParty(
"https://"+GetEffectiveDomain(r),
NewCredentialStorage(client),
)
}
// GetEffectiveDomain returns the effective domain for an HTTP request.
func GetEffectiveDomain(r *http.Request) string {
h, _, err := net.SplitHostPort(r.Host)
if err != nil {
h = r.Host
}
if tld, err := publicsuffix.EffectiveTLDPlusOne(h); err == nil {
return tld
}
return h
}

View file

@ -0,0 +1,31 @@
package webauthnutil
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetEffectiveDomain(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
in string
expect string
}{
{"https://www.example.com/some/path", "example.com"},
{"https://www.example.com:8080/some/path", "example.com"},
{"https://www.subdomain.example.com/some/path", "example.com"},
{"https://example.com/some/path", "example.com"},
} {
tc := tc
t.Run(tc.expect, func(t *testing.T) {
t.Parallel()
r, err := http.NewRequest(http.MethodGet, tc.in, nil)
require.NoError(t, err)
assert.Equal(t, tc.expect, GetEffectiveDomain(r))
})
}
}