userinfo: add webauthn buttons to user info page (#3075)

* userinfo: add webauthn buttons to user info page

* use new buttons on original page

* fix test
This commit is contained in:
Caleb Doxsey 2022-02-23 10:08:24 -07:00 committed by GitHub
parent 38c7089642
commit 35f697e491
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 423 additions and 288 deletions

View file

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/pkg/cryptutil"
@ -33,9 +34,10 @@ func ValidateOptions(o *config.Options) error {
// Authenticate contains data required to run the authenticate service.
type Authenticate struct {
cfg *authenticateConfig
options *config.AtomicOptions
state *atomicAuthenticateState
cfg *authenticateConfig
options *config.AtomicOptions
state *atomicAuthenticateState
webauthn *webauthn.Handler
}
// New validates and creates a new authenticate service from a set of Options.
@ -45,6 +47,7 @@ func New(cfg *config.Config, options ...Option) (*Authenticate, error) {
options: config.NewAtomicOptions(),
state: newAtomicAuthenticateState(newAuthenticateState()),
}
a.webauthn = webauthn.New(a.getWebauthnState)
state, err := newAuthenticateStateFromConfig(cfg)
if err != nil {

View file

@ -17,6 +17,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/pomerium/csrf"
"github.com/pomerium/pomerium/authenticate/handlers"
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
"github.com/pomerium/pomerium/internal/httputil"
@ -96,7 +97,7 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState))
sr.Path("/webauthn").Handler(a.webauthn)
sr.Path("/device-enrolled").Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
handlers.DeviceEnrolled().ServeHTTP(w, r)
return nil
@ -561,6 +562,8 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
groups = append(groups, pbDirectoryGroup)
}
creationOptions, requestOptions, _ := a.webauthn.GetOptions(ctx)
handlers.UserInfo(handlers.UserInfoData{
CSRFToken: csrf.Token(r),
DirectoryGroups: groups,
@ -568,7 +571,10 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
IsImpersonated: isImpersonated,
Session: pbSession,
User: pbUser,
WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()),
WebAuthnCreationOptions: creationOptions,
WebAuthnRequestOptions: requestOptions,
WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()),
}).ServeHTTP(w, r)
return nil
}

View file

@ -11,6 +11,7 @@ import (
"github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user"
"github.com/pomerium/pomerium/ui"
"github.com/pomerium/webauthn"
)
// UserInfoData is the data for the UserInfo page.
@ -21,7 +22,10 @@ type UserInfoData struct {
IsImpersonated bool
Session *session.Session
User *user.User
WebAuthnURL string
WebAuthnCreationOptions *webauthn.PublicKeyCredentialCreationOptions
WebAuthnRequestOptions *webauthn.PublicKeyCredentialRequestOptions
WebAuthnURL string
}
// ToJSON converts the data into a JSON map.
@ -45,6 +49,8 @@ func (data UserInfoData) ToJSON() map[string]interface{} {
if bs, err := protojson.Marshal(data.User); err == nil {
m["user"] = json.RawMessage(bs)
}
m["webAuthnCreationOptions"] = data.WebAuthnCreationOptions
m["webAuthnRequestOptions"] = data.WebAuthnRequestOptions
m["webAuthnUrl"] = data.WebAuthnURL
return m
}

View file

@ -72,11 +72,50 @@ func New(getState StateProvider) *Handler {
}
}
// GetOptions returns the creation and request options for WebAuthn.
func (h *Handler) GetOptions(ctx context.Context) (
creationOptions *webauthn.PublicKeyCredentialCreationOptions,
requestOptions *webauthn.PublicKeyCredentialRequestOptions,
err error,
) {
state, err := h.getState(ctx)
if err != nil {
return nil, nil, err
}
return h.getOptions(ctx, state, webauthnutil.DefaultDeviceType)
}
// ServeHTTP serves the HTTP handler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
httputil.HandlerFunc(h.handle).ServeHTTP(w, r)
}
func (h *Handler) getOptions(ctx context.Context, state *State, deviceTypeParam string) (
creationOptions *webauthn.PublicKeyCredentialCreationOptions,
requestOptions *webauthn.PublicKeyCredentialRequestOptions,
err error,
) {
// get the user information
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
if err != nil {
return nil, nil, err
}
// get the device credentials
knownDeviceCredentials, err := getKnownDeviceCredentials(ctx, state.Client, u.GetDeviceCredentialIds()...)
if err != nil {
return nil, nil, err
}
// get the stored device type
deviceType := webauthnutil.GetDeviceType(ctx, state.Client, deviceTypeParam)
creationOptions = webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u)
requestOptions = webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials)
return creationOptions, requestOptions, nil
}
func (h *Handler) handle(w http.ResponseWriter, r *http.Request) error {
s, err := h.getState(r.Context())
if err != nil {
@ -351,24 +390,11 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat
return errMissingDeviceType
}
// get the user information
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
creationOptions, requestOptions, err := h.getOptions(ctx, state, deviceTypeParam)
if err != nil {
return err
}
// get the device credentials
knownDeviceCredentials, err := getKnownDeviceCredentials(ctx, state.Client, u.GetDeviceCredentialIds()...)
if err != nil {
return err
}
// get the stored device type
deviceType := webauthnutil.GetDeviceType(ctx, state.Client, deviceTypeParam)
creationOptions := webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u)
requestOptions := webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials)
return ui.ServePage(w, r, "WebAuthnRegistration", map[string]interface{}{
"creationOptions": creationOptions,
"requestOptions": requestOptions,

View file

@ -24,6 +24,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws"
@ -748,6 +749,7 @@ func TestAuthenticate_userInfo(t *testing.T) {
directoryClient: new(mockDirectoryServiceClient),
}),
}
a.webauthn = webauthn.New(a.getWebauthnState)
r := httptest.NewRequest(tt.method, tt.url.String(), nil)
state, err := tt.sessionStore.LoadSession(r)
if err != nil {