mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-06 10:21:05 +02:00
authenticate: add support for webauthn (#2688)
* authenticate: add support for webauthn * remove rfc4648 library due to missing LICENSE * fix test * put state function in separate function
This commit is contained in:
parent
3051ad77e0
commit
1162585471
12 changed files with 855 additions and 18 deletions
|
@ -7,6 +7,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/frontend"
|
"github.com/pomerium/pomerium/internal/frontend"
|
||||||
|
@ -125,3 +126,36 @@ func (a *Authenticate) updateProvider(cfg *config.Config) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Authenticate) getWebAuthnURL(values url.Values) (*url.URL, error) {
|
||||||
|
uri, err := a.options.Load().GetAuthenticateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = uri.ResolveReference(&url.URL{
|
||||||
|
Path: "/.pomerium/webauthn",
|
||||||
|
RawQuery: buildURLValues(values, url.Values{
|
||||||
|
urlutil.QueryDeviceType: {"default"},
|
||||||
|
urlutil.QueryEnrollmentToken: nil,
|
||||||
|
urlutil.QueryRedirectURI: {uri.ResolveReference(&url.URL{
|
||||||
|
Path: "/.pomerium/",
|
||||||
|
}).String()},
|
||||||
|
}).Encode(),
|
||||||
|
})
|
||||||
|
return urlutil.NewSignedURL(a.state.Load().sharedKey, uri).Sign(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildURLValues creates a new url.Values map by traversing the keys in `defaults` and using the values
|
||||||
|
// from `values` if they exist, otherwise the provided defaults
|
||||||
|
func buildURLValues(values, defaults url.Values) url.Values {
|
||||||
|
result := make(url.Values)
|
||||||
|
for k, vs := range defaults {
|
||||||
|
if values.Has(k) {
|
||||||
|
result[k] = values[k]
|
||||||
|
} else if vs != nil {
|
||||||
|
result[k] = vs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/identity"
|
"github.com/pomerium/pomerium/internal/identity"
|
||||||
"github.com/pomerium/pomerium/internal/identity/manager"
|
"github.com/pomerium/pomerium/internal/identity/manager"
|
||||||
|
@ -28,6 +29,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/device"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
@ -93,6 +95,7 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
|
||||||
sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
|
sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
|
||||||
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
|
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
|
||||||
sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut))
|
sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut))
|
||||||
|
sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticate) mountWellKnown(r *mux.Router) {
|
func (a *Authenticate) mountWellKnown(r *mux.Router) {
|
||||||
|
@ -451,25 +454,20 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
s.ID = uuid.New().String()
|
s.ID = uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
isImpersonated := false
|
pbSession, isImpersonated, err := a.getCurrentSession(ctx)
|
||||||
pbSession, err := session.Get(ctx, state.dataBrokerClient, s.ID)
|
|
||||||
if pbSession.GetImpersonateSessionId() != "" {
|
|
||||||
pbSession, err = session.Get(ctx, state.dataBrokerClient, pbSession.GetImpersonateSessionId())
|
|
||||||
isImpersonated = true
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pbSession = &session.Session{
|
pbSession = &session.Session{
|
||||||
Id: s.ID,
|
Id: s.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pbUser, err := user.Get(ctx, state.dataBrokerClient, pbSession.GetUserId())
|
pbUser, err := a.getUser(ctx, pbSession.GetUserId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pbUser = &user.User{
|
pbUser = &user.User{
|
||||||
Id: pbSession.GetUserId(),
|
Id: pbSession.GetUserId(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pbDirectoryUser, err := directory.GetUser(ctx, state.dataBrokerClient, pbSession.GetUserId())
|
pbDirectoryUser, err := a.getDirectoryUser(ctx, pbSession.GetUserId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pbDirectoryUser = &directory.User{
|
pbDirectoryUser = &directory.User{
|
||||||
Id: pbSession.GetUserId(),
|
Id: pbSession.GetUserId(),
|
||||||
|
@ -493,15 +491,29 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
return fmt.Errorf("invalid signout url: %w", err)
|
return fmt.Errorf("invalid signout url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webAuthnURL, err := a.getWebAuthnURL(r.URL.Query())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid webauthn url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceCredentials []*device.Credential
|
||||||
|
for _, id := range pbUser.GetDeviceCredentialIds() {
|
||||||
|
deviceCredentials = append(deviceCredentials, &device.Credential{
|
||||||
|
Id: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
input := map[string]interface{}{
|
input := map[string]interface{}{
|
||||||
"IsImpersonated": isImpersonated,
|
"IsImpersonated": isImpersonated,
|
||||||
"State": s, // local session state (cookie, header, etc)
|
"State": s, // local session state (cookie, header, etc)
|
||||||
"Session": pbSession, // current access, refresh, id token
|
"Session": pbSession, // current access, refresh, id token
|
||||||
"User": pbUser, // user details inferred from oidc id_token
|
"User": pbUser, // user details inferred from oidc id_token
|
||||||
"DirectoryUser": pbDirectoryUser, // user details inferred from idp directory
|
"DeviceCredentials": deviceCredentials,
|
||||||
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
"DirectoryUser": pbDirectoryUser, // user details inferred from idp directory
|
||||||
"csrfField": csrf.TemplateField(r),
|
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
||||||
"SignOutURL": signoutURL,
|
"csrfField": csrf.TemplateField(r),
|
||||||
|
"SignOutURL": signoutURL,
|
||||||
|
"WebAuthnURL": webAuthnURL,
|
||||||
}
|
}
|
||||||
return a.templates.ExecuteTemplate(w, "userInfo.html", input)
|
return a.templates.ExecuteTemplate(w, "userInfo.html", input)
|
||||||
}
|
}
|
||||||
|
@ -614,3 +626,49 @@ func (a *Authenticate) getSignOutURL(r *http.Request) (*url.URL, error) {
|
||||||
}
|
}
|
||||||
return urlutil.NewSignedURL(a.state.Load().sharedKey, uri).Sign(), nil
|
return urlutil.NewSignedURL(a.state.Load().sharedKey, uri).Sign(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Authenticate) getCurrentSession(ctx context.Context) (s *session.Session, isImpersonated bool, err error) {
|
||||||
|
client := a.state.Load().dataBrokerClient
|
||||||
|
|
||||||
|
sessionState, err := a.getSessionFromCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isImpersonated = false
|
||||||
|
s, err = session.Get(ctx, client, sessionState.ID)
|
||||||
|
if s.GetImpersonateSessionId() != "" {
|
||||||
|
s, err = session.Get(ctx, client, s.GetImpersonateSessionId())
|
||||||
|
isImpersonated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, isImpersonated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticate) getUser(ctx context.Context, userID string) (*user.User, error) {
|
||||||
|
client := a.state.Load().dataBrokerClient
|
||||||
|
|
||||||
|
return user.Get(ctx, client, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticate) getDirectoryUser(ctx context.Context, userID string) (*directory.User, error) {
|
||||||
|
client := a.state.Load().dataBrokerClient
|
||||||
|
|
||||||
|
return directory.GetUser(ctx, client, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticate) getWebauthnState(ctx context.Context) (*webauthn.State, error) {
|
||||||
|
state := a.state.Load()
|
||||||
|
|
||||||
|
s, _, err := a.getCurrentSession(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &webauthn.State{
|
||||||
|
SharedKey: state.sharedKey,
|
||||||
|
Client: state.dataBrokerClient,
|
||||||
|
Session: s,
|
||||||
|
RelyingParty: state.webauthnRelyingParty,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
409
authenticate/handlers/webauthn/webauthn.go
Normal file
409
authenticate/handlers/webauthn/webauthn.go
Normal file
|
@ -0,0 +1,409 @@
|
||||||
|
// Package webauthn contains handlers for the WebAuthn flow in authenticate.
|
||||||
|
package webauthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pomerium/csrf"
|
||||||
|
"github.com/pomerium/webauthn"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/frontend"
|
||||||
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
|
"github.com/pomerium/pomerium/internal/middleware"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/device"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
"github.com/pomerium/pomerium/pkg/webauthnutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxAuthenticateResponses = 5
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingDeviceType = httputil.NewError(http.StatusBadRequest, errors.New("device_type is a required parameter"))
|
||||||
|
errMissingRedirectURI = httputil.NewError(http.StatusBadRequest, errors.New("pomerium_redirect_uri is a required parameter"))
|
||||||
|
)
|
||||||
|
|
||||||
|
// State is the state needed by the Handler to handle requests.
|
||||||
|
type State struct {
|
||||||
|
SharedKey []byte
|
||||||
|
Client databroker.DataBrokerServiceClient
|
||||||
|
Session *session.Session
|
||||||
|
RelyingParty *webauthn.RelyingParty
|
||||||
|
}
|
||||||
|
|
||||||
|
// A StateProvider provides state for the handler.
|
||||||
|
type StateProvider = func(context.Context) (*State, error)
|
||||||
|
|
||||||
|
// Handler is the WebAuthn device handler.
|
||||||
|
type Handler struct {
|
||||||
|
getState StateProvider
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Handler.
|
||||||
|
func New(getState StateProvider) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
getState: getState,
|
||||||
|
templates: template.Must(frontend.NewTemplates()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
s, err := h.getState(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = middleware.ValidateRequestURL(r, s.SharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
return h.handleView(w, r, s)
|
||||||
|
case r.FormValue("action") == "authenticate":
|
||||||
|
return h.handleAuthenticate(w, r, s)
|
||||||
|
case r.FormValue("action") == "register":
|
||||||
|
return h.handleRegister(w, r, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return httputil.NewError(http.StatusNotFound, errors.New(http.StatusText(http.StatusNotFound)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleAuthenticate(w http.ResponseWriter, r *http.Request, state *State) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
deviceTypeParam := r.FormValue(urlutil.QueryDeviceType)
|
||||||
|
if deviceTypeParam == "" {
|
||||||
|
return errMissingDeviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURIParam := r.FormValue(urlutil.QueryRedirectURI)
|
||||||
|
if redirectURIParam == "" {
|
||||||
|
return errMissingRedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
responseParam := r.FormValue("authenticate_response")
|
||||||
|
var credential webauthn.PublicKeyAssertionCredential
|
||||||
|
err := json.Unmarshal([]byte(responseParam), &credential)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, errors.New("invalid authenticate response"))
|
||||||
|
}
|
||||||
|
credentialJSON, err := json.Marshal(credential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the user information
|
||||||
|
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the stored device type
|
||||||
|
deviceType, err := device.GetType(ctx, state.Client, deviceTypeParam)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the device credentials
|
||||||
|
knownDeviceCredentials, err := getKnownDeviceCredentials(ctx, state.Client, u.GetDeviceCredentialIds()...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions, err := webauthnutil.GetRequestOptionsForCredential(
|
||||||
|
state.SharedKey,
|
||||||
|
deviceType,
|
||||||
|
knownDeviceCredentials,
|
||||||
|
&credential,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, fmt.Errorf("invalid register options: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCredential, err := state.RelyingParty.VerifyAuthenticationCeremony(
|
||||||
|
ctx,
|
||||||
|
requestOptions,
|
||||||
|
&credential,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, fmt.Errorf("error verifying registration: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the authenticate response
|
||||||
|
for _, deviceCredential := range knownDeviceCredentials {
|
||||||
|
webauthnCredential := deviceCredential.GetWebauthn()
|
||||||
|
if webauthnCredential == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(webauthnCredential.Id, serverCredential.ID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the response to the list and cap it, removing the oldest responses
|
||||||
|
webauthnCredential.AuthenticateResponse = append(webauthnCredential.AuthenticateResponse, credentialJSON)
|
||||||
|
for len(webauthnCredential.AuthenticateResponse) > maxAuthenticateResponses {
|
||||||
|
webauthnCredential.AuthenticateResponse = webauthnCredential.AuthenticateResponse[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the updated device credential
|
||||||
|
err = device.PutCredential(ctx, state.Client, deviceCredential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the session
|
||||||
|
state.Session.DeviceCredentialId = webauthnutil.GetDeviceCredentialID(serverCredential.ID)
|
||||||
|
_, err = session.Put(ctx, state.Client, state.Session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect
|
||||||
|
httputil.Redirect(w, r, redirectURIParam, http.StatusFound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleRegister(w http.ResponseWriter, r *http.Request, state *State) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
deviceTypeParam := r.FormValue(urlutil.QueryDeviceType)
|
||||||
|
if deviceTypeParam == "" {
|
||||||
|
return errMissingDeviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURIParam := r.FormValue(urlutil.QueryRedirectURI)
|
||||||
|
if redirectURIParam == "" {
|
||||||
|
return errMissingRedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
responseParam := r.FormValue("register_response")
|
||||||
|
var credential webauthn.PublicKeyCreationCredential
|
||||||
|
err := json.Unmarshal([]byte(responseParam), &credential)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, errors.New("invalid register response"))
|
||||||
|
}
|
||||||
|
credentialJSON, err := json.Marshal(credential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the user information
|
||||||
|
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the stored device type
|
||||||
|
deviceType, err := webauthnutil.GetDeviceType(ctx, state.Client, deviceTypeParam)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
creationOptions, err := webauthnutil.GetCreationOptionsForCredential(
|
||||||
|
state.SharedKey,
|
||||||
|
deviceType,
|
||||||
|
u,
|
||||||
|
&credential,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, fmt.Errorf("invalid register options: %w", err))
|
||||||
|
}
|
||||||
|
creationOptionsJSON, err := json.Marshal(creationOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCredential, err := state.RelyingParty.VerifyRegistrationCeremony(
|
||||||
|
ctx,
|
||||||
|
creationOptions,
|
||||||
|
&credential,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, fmt.Errorf("error verifying registration: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceEnrollment, err := getOrCreateDeviceEnrollment(ctx, r, state, u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the credential
|
||||||
|
deviceCredential := &device.Credential{
|
||||||
|
Id: webauthnutil.GetDeviceCredentialID(serverCredential.ID),
|
||||||
|
TypeId: deviceType.GetId(),
|
||||||
|
EnrollmentId: deviceEnrollment.GetId(),
|
||||||
|
UserId: u.GetId(),
|
||||||
|
Specifier: &device.Credential_Webauthn{
|
||||||
|
Webauthn: &device.Credential_WebAuthn{
|
||||||
|
Id: serverCredential.ID,
|
||||||
|
PublicKey: serverCredential.PublicKey,
|
||||||
|
|
||||||
|
RegisterOptions: creationOptionsJSON,
|
||||||
|
RegisterResponse: credentialJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = device.PutCredential(ctx, state.Client, deviceCredential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the user
|
||||||
|
u.DeviceCredentialIds = append(u.DeviceCredentialIds, deviceCredential.GetId())
|
||||||
|
_, err = user.Put(ctx, state.Client, u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the session
|
||||||
|
state.Session.DeviceCredentialId = deviceCredential.GetId()
|
||||||
|
_, err = session.Put(ctx, state.Client, state.Session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect
|
||||||
|
httputil.Redirect(w, r, redirectURIParam, http.StatusFound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *State) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
deviceTypeParam := r.FormValue(urlutil.QueryDeviceType)
|
||||||
|
if deviceTypeParam == "" {
|
||||||
|
return errMissingDeviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the user information
|
||||||
|
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
|
||||||
|
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, err := webauthnutil.GetDeviceType(ctx, state.Client, deviceTypeParam)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
creationOptions := webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u)
|
||||||
|
requestOptions := webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = h.templates.ExecuteTemplate(&buf, "webauthn.html", map[string]interface{}{
|
||||||
|
"csrfField": csrf.TemplateField(r),
|
||||||
|
"Data": map[string]interface{}{
|
||||||
|
"creationOptions": creationOptions,
|
||||||
|
"requestOptions": requestOptions,
|
||||||
|
},
|
||||||
|
"SelfURL": r.URL.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = io.Copy(w, &buf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKnownDeviceCredentials(
|
||||||
|
ctx context.Context,
|
||||||
|
client databroker.DataBrokerServiceClient,
|
||||||
|
deviceCredentialIDs ...string,
|
||||||
|
) ([]*device.Credential, error) {
|
||||||
|
var knownDeviceCredentials []*device.Credential
|
||||||
|
for _, deviceCredentialID := range deviceCredentialIDs {
|
||||||
|
deviceCredential, err := device.GetCredential(ctx, client, deviceCredentialID)
|
||||||
|
if status.Code(err) == codes.NotFound {
|
||||||
|
// ignore missing devices
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, httputil.NewError(http.StatusInternalServerError,
|
||||||
|
fmt.Errorf("error retrieving device credential: %w", err))
|
||||||
|
}
|
||||||
|
knownDeviceCredentials = append(knownDeviceCredentials, deviceCredential)
|
||||||
|
}
|
||||||
|
return knownDeviceCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrCreateDeviceEnrollment(
|
||||||
|
ctx context.Context,
|
||||||
|
r *http.Request,
|
||||||
|
state *State,
|
||||||
|
u *user.User,
|
||||||
|
) (*device.Enrollment, error) {
|
||||||
|
var deviceEnrollment *device.Enrollment
|
||||||
|
|
||||||
|
enrollmentTokenParam := r.FormValue(urlutil.QueryEnrollmentToken)
|
||||||
|
if enrollmentTokenParam == "" {
|
||||||
|
// create a new enrollment
|
||||||
|
deviceEnrollment = &device.Enrollment{
|
||||||
|
Id: uuid.New().String(),
|
||||||
|
UserId: u.GetId(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// use an existing enrollment
|
||||||
|
deviceEnrollmentID, err := webauthnutil.ParseAndVerifyEnrollmentToken(state.SharedKey, enrollmentTokenParam)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httputil.NewError(http.StatusBadRequest, fmt.Errorf("invalid enrollment token: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceEnrollment, err = device.GetEnrollment(ctx, state.Client, deviceEnrollmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if deviceEnrollment.GetUserId() != u.GetId() {
|
||||||
|
return nil, httputil.NewError(http.StatusForbidden, fmt.Errorf("invalid enrollment token: wrong user id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if deviceEnrollment.GetEnrolledAt().IsValid() {
|
||||||
|
return nil, httputil.NewError(http.StatusForbidden, fmt.Errorf("invalid enrollment token: already used for existing credential"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceEnrollment.EnrolledAt = timestamppb.Now()
|
||||||
|
deviceEnrollment.UserAgent = r.UserAgent()
|
||||||
|
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||||
|
deviceEnrollment.IpAddress = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
err := device.PutEnrollment(ctx, state.Client, deviceEnrollment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return deviceEnrollment, nil
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v3"
|
"github.com/go-jose/go-jose/v3"
|
||||||
|
"github.com/pomerium/webauthn"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/encoding"
|
"github.com/pomerium/pomerium/internal/encoding"
|
||||||
|
@ -23,6 +24,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/pkg/grpc"
|
"github.com/pomerium/pomerium/pkg/grpc"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||||
|
"github.com/pomerium/pomerium/pkg/webauthnutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var outboundGRPCConnection = new(grpc.CachedOutboundGRPClientConn)
|
var outboundGRPCConnection = new(grpc.CachedOutboundGRPClientConn)
|
||||||
|
@ -52,6 +54,8 @@ type authenticateState struct {
|
||||||
|
|
||||||
dataBrokerClient databroker.DataBrokerServiceClient
|
dataBrokerClient databroker.DataBrokerServiceClient
|
||||||
directoryClient directory.DirectoryServiceClient
|
directoryClient directory.DirectoryServiceClient
|
||||||
|
|
||||||
|
webauthnRelyingParty *webauthn.RelyingParty
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthenticateState() *authenticateState {
|
func newAuthenticateState() *authenticateState {
|
||||||
|
@ -158,6 +162,11 @@ func newAuthenticateStateFromConfig(cfg *config.Config) (*authenticateState, err
|
||||||
state.dataBrokerClient = databroker.NewDataBrokerServiceClient(dataBrokerConn)
|
state.dataBrokerClient = databroker.NewDataBrokerServiceClient(dataBrokerConn)
|
||||||
state.directoryClient = directory.NewDirectoryServiceClient(dataBrokerConn)
|
state.directoryClient = directory.NewDirectoryServiceClient(dataBrokerConn)
|
||||||
|
|
||||||
|
state.webauthnRelyingParty = webauthn.NewRelyingParty(
|
||||||
|
authenticateURL.String(),
|
||||||
|
webauthnutil.NewCredentialStorage(state.dataBrokerClient),
|
||||||
|
)
|
||||||
|
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,39 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="category white box">
|
||||||
|
<div class="messages">
|
||||||
|
<div class="box-inner">
|
||||||
|
<div class="category-header clearfix">
|
||||||
|
<span class="category-title">Device Credentials</span>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
{{if .DeviceCredentials}}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .DeviceCredentials}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Id}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
No device credentials found!
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="category-link">
|
||||||
|
Register device with <a href="{{.WebAuthnURL}}">WebAuthn</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://pomerium.com/">Home</a></li>
|
<li><a href="https://pomerium.com/">Home</a></li>
|
||||||
|
|
67
internal/frontend/assets/html/webauthn.go.html
Normal file
67
internal/frontend/assets/html/webauthn.go.html
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
{{define "webauthn.html"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" charset="utf-8">
|
||||||
|
<head>
|
||||||
|
{{template "header.html"}}
|
||||||
|
<title>WebAuthn</title>
|
||||||
|
<script>
|
||||||
|
window.PomeriumData = {{.Data}};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="inner">
|
||||||
|
<div class="header clearfix">
|
||||||
|
<div class="heading"></div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="white box">
|
||||||
|
<div class="largestatus">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<span class="title">WebAuthn Registration</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="category white box webauthn">
|
||||||
|
<div class="messages">
|
||||||
|
<div class="box-inner">
|
||||||
|
<form action="{{.SelfURL}}" method="post">
|
||||||
|
{{.csrfField}}
|
||||||
|
<input type="hidden" name="action" value="register" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
id="register_response"
|
||||||
|
name="register_response"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="button"
|
||||||
|
type="submit"
|
||||||
|
id="register_button"
|
||||||
|
value="Register New Device"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<form action="{{.SelfURL}}" method="post">
|
||||||
|
{{.csrfField}}
|
||||||
|
<input type="hidden" name="action" value="authenticate" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
id="authenticate_response"
|
||||||
|
name="authenticate_response"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="button"
|
||||||
|
type="submit"
|
||||||
|
id="authenticate_button"
|
||||||
|
value="Authenticate Existing Device"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/.pomerium/assets/js/webauthn.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
92
internal/frontend/assets/js/base64.mjs
Normal file
92
internal/frontend/assets/js/base64.mjs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
// The MIT License (MIT)
|
||||||
|
|
||||||
|
// Copyright (c) 2020 Blake Embrey (hello@blakeembrey.com)
|
||||||
|
// Copyright (c) 2012 Niklas von Hertzen (https://github.com/niklasvh/base64-arraybuffer)
|
||||||
|
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original source: https://github.com/niklasvh/base64-arraybuffer.
|
||||||
|
*/
|
||||||
|
const base64Chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
const base64UrlChars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
const base64Lookup = new Uint8Array(256);
|
||||||
|
for (let i = 0; i < base64Chars.length; i++) {
|
||||||
|
base64Lookup[base64Chars.charCodeAt(i)] = i;
|
||||||
|
}
|
||||||
|
// Support base64url.
|
||||||
|
base64Lookup[45 /* - */] = 62;
|
||||||
|
base64Lookup[95 /* _ */] = 63;
|
||||||
|
/**
|
||||||
|
* Encode an `ArrayBuffer` to base64.
|
||||||
|
*/
|
||||||
|
function encode(buffer, chars = base64Chars, padding = "=") {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const length = bytes.length;
|
||||||
|
let base64 = "";
|
||||||
|
for (let i = 0; i < length; i += 3) {
|
||||||
|
base64 += chars[bytes[i] >> 2];
|
||||||
|
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||||
|
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||||
|
base64 += chars[bytes[i + 2] & 63];
|
||||||
|
}
|
||||||
|
if (length % 3 === 2) {
|
||||||
|
base64 = base64.slice(0, base64.length - 1) + padding;
|
||||||
|
} else if (length % 3 === 1) {
|
||||||
|
base64 = base64.slice(0, base64.length - 2) + padding + padding;
|
||||||
|
}
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode using the base64url variant.
|
||||||
|
*/
|
||||||
|
function encodeUrl(buffer) {
|
||||||
|
return encode(buffer, base64UrlChars, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a base64 encoded string.
|
||||||
|
*/
|
||||||
|
function decode(base64, lookup = base64Lookup) {
|
||||||
|
const length = base64.length;
|
||||||
|
let bufferLength = Math.floor(base64.length * 0.75);
|
||||||
|
let p = 0;
|
||||||
|
if (base64[length - 1] === "=") {
|
||||||
|
bufferLength--;
|
||||||
|
if (base64[length - 2] === "=") {
|
||||||
|
bufferLength--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bytes = new Uint8Array(bufferLength);
|
||||||
|
for (let i = 0; i < length; i += 4) {
|
||||||
|
const encoded1 = lookup[base64.charCodeAt(i)];
|
||||||
|
const encoded2 = lookup[base64.charCodeAt(i + 1)];
|
||||||
|
const encoded3 = lookup[base64.charCodeAt(i + 2)];
|
||||||
|
const encoded4 = lookup[base64.charCodeAt(i + 3)];
|
||||||
|
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||||
|
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||||
|
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { encode, encodeUrl, decode };
|
119
internal/frontend/assets/js/webauthn.mjs
Normal file
119
internal/frontend/assets/js/webauthn.mjs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import {
|
||||||
|
encodeUrl as base64encode,
|
||||||
|
decode as base64decode,
|
||||||
|
} from "./base64.mjs";
|
||||||
|
|
||||||
|
function decode(raw) {
|
||||||
|
return base64decode(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(raw) {
|
||||||
|
return base64encode(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticate(requestOptions) {
|
||||||
|
const credential = await navigator.credentials.get({
|
||||||
|
publicKey: {
|
||||||
|
allowCredentials: requestOptions.allowCredentials.map((c) => ({
|
||||||
|
type: c.type,
|
||||||
|
id: decode(c.id),
|
||||||
|
})),
|
||||||
|
challenge: decode(requestOptions.challenge),
|
||||||
|
timeout: requestOptions.timeout,
|
||||||
|
userVerification: requestOptions.userVerification,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const inputEl = document.getElementById("authenticate_response");
|
||||||
|
inputEl.value = JSON.stringify({
|
||||||
|
id: credential.id,
|
||||||
|
type: credential.type,
|
||||||
|
rawId: encode(credential.rawId),
|
||||||
|
response: {
|
||||||
|
authenticatorData: encode(credential.response.authenticatorData),
|
||||||
|
clientDataJSON: encode(credential.response.clientDataJSON),
|
||||||
|
signature: encode(credential.response.signature),
|
||||||
|
userHandle: encode(credential.response.userHandle),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
inputEl.parentElement.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(creationOptions) {
|
||||||
|
const credential = await navigator.credentials.create({
|
||||||
|
publicKey: {
|
||||||
|
attestation: creationOptions.attestation || undefined,
|
||||||
|
authenticatorSelection: {
|
||||||
|
authenticatorAttachment:
|
||||||
|
creationOptions.authenticatorSelection.authenticatorAttachment ||
|
||||||
|
undefined,
|
||||||
|
requireResidentKey:
|
||||||
|
creationOptions.authenticatorSelection.requireResidentKey ||
|
||||||
|
undefined,
|
||||||
|
residentKey: creationOptions.authenticatorSelection.residentKey,
|
||||||
|
userVerification:
|
||||||
|
creationOptions.authenticatorSelection.userVerification || undefined,
|
||||||
|
},
|
||||||
|
challenge: decode(creationOptions.challenge),
|
||||||
|
pubKeyCredParams: creationOptions.pubKeyCredParams.map((p) => ({
|
||||||
|
type: p.type,
|
||||||
|
alg: p.alg,
|
||||||
|
})),
|
||||||
|
rp: {
|
||||||
|
name: creationOptions.rp.name,
|
||||||
|
},
|
||||||
|
timeout: creationOptions.timeout,
|
||||||
|
user: {
|
||||||
|
id: decode(creationOptions.user.id),
|
||||||
|
name: creationOptions.user.name,
|
||||||
|
displayName: creationOptions.user.displayName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const inputEl = document.getElementById("register_response");
|
||||||
|
inputEl.value = JSON.stringify({
|
||||||
|
id: credential.id,
|
||||||
|
type: credential.type,
|
||||||
|
rawId: encode(credential.rawId),
|
||||||
|
response: {
|
||||||
|
attestationObject: encode(credential.response.attestationObject),
|
||||||
|
clientDataJSON: encode(credential.response.clientDataJSON),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
inputEl.parentElement.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (!("PomeriumData" in window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions = window.PomeriumData.requestOptions;
|
||||||
|
const authenticateButton = document.getElementById("authenticate_button");
|
||||||
|
if (authenticateButton) {
|
||||||
|
if (
|
||||||
|
requestOptions.allowCredentials &&
|
||||||
|
requestOptions.allowCredentials.length > 0
|
||||||
|
) {
|
||||||
|
authenticateButton.addEventListener("click", function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
authenticate(requestOptions);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authenticateButton.addEventListener("click", function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
});
|
||||||
|
authenticateButton.setAttribute("disabled", "DISABLED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const creationOptions = window.PomeriumData.creationOptions;
|
||||||
|
const registerButton = document.getElementById("register_button");
|
||||||
|
if (registerButton) {
|
||||||
|
registerButton.addEventListener("click", function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
register(creationOptions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
|
@ -476,3 +476,17 @@ a.button {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
transition: box-shadow 150ms ease-in-out;
|
transition: box-shadow 150ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled, input:disabled {
|
||||||
|
cursor: default;
|
||||||
|
background:#cccccc;
|
||||||
|
color: #838383;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webauthn .box-inner {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webauthn form {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -31,7 +31,7 @@ const (
|
||||||
// by default includes profile photo exceptions for supported identity providers.
|
// by default includes profile photo exceptions for supported identity providers.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
|
||||||
var HeadersContentSecurityPolicy = map[string]string{
|
var HeadersContentSecurityPolicy = map[string]string{
|
||||||
"Content-Security-Policy": "default-src 'none'; style-src 'self' data:; img-src * data:;",
|
"Content-Security-Policy": "default-src 'none'; style-src 'self' data:; img-src * data:; script-src 'self' 'unsafe-inline'",
|
||||||
"Referrer-Policy": "Same-origin",
|
"Referrer-Policy": "Same-origin",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ package urlutil
|
||||||
// conjunction with a HMAC to ensure authenticity.
|
// conjunction with a HMAC to ensure authenticity.
|
||||||
const (
|
const (
|
||||||
QueryCallbackURI = "pomerium_callback_uri"
|
QueryCallbackURI = "pomerium_callback_uri"
|
||||||
|
QueryDeviceType = "pomerium_device_type"
|
||||||
|
QueryEnrollmentToken = "pomerium_enrollment_token" //nolint
|
||||||
QueryIsProgrammatic = "pomerium_programmatic"
|
QueryIsProgrammatic = "pomerium_programmatic"
|
||||||
QueryForwardAuth = "pomerium_forward_auth"
|
QueryForwardAuth = "pomerium_forward_auth"
|
||||||
QueryPomeriumJWT = "pomerium_jwt"
|
QueryPomeriumJWT = "pomerium_jwt"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue