pkg/provider/okta/okta_webauthn.go (123 lines of code) (raw):

package okta import ( "errors" "fmt" "time" "github.com/marshallbrekka/go-u2fhost" ) const ( MaxOpenRetries = 10 RetryDelayMS = 200 * time.Millisecond ) var ( errNoDeviceFound = fmt.Errorf("no U2F devices found. device might not be plugged in") ) // FidoClient represents a challenge and the device used to respond type FidoClient struct { ChallengeNonce string AppID string Version string Device u2fhost.Device KeyHandle string StateToken string } // SignedAssertion is passed back to Okta as response type SignedAssertion struct { StateToken string `json:"stateToken"` ClientData string `json:"clientData"` SignatureData string `json:"signatureData"` AuthenticatorData string `json:"authenticatorData"` } // DeviceFinder is used to mock out finding devices type DeviceFinder interface { findDevice() (u2fhost.Device, error) } // U2FDevice is used to support mocking this device with mockery https://github.com/vektra/mockery/issues/210#issuecomment-485026348 type U2FDevice interface { u2fhost.Device } // NewFidoClient returns a new initialized FIDO1-based WebAuthnClient, representing a single device func NewFidoClient(challengeNonce, appID, version, keyHandle, stateToken string, deviceFinder DeviceFinder) (FidoClient, error) { var device u2fhost.Device var err error retryCount := 0 for retryCount < MaxOpenRetries { device, err = deviceFinder.findDevice() if err != nil { if err == errNoDeviceFound { return FidoClient{}, err } retryCount++ time.Sleep(RetryDelayMS) continue } return FidoClient{ Device: device, ChallengeNonce: challengeNonce, AppID: appID, Version: version, KeyHandle: keyHandle, StateToken: stateToken, }, nil } return FidoClient{}, fmt.Errorf("failed to create client: %s. exceeded max retries of %d", err, MaxOpenRetries) } // ChallengeU2F takes a FidoClient and returns a signed assertion to send to Okta func (d *FidoClient) ChallengeU2F() (*SignedAssertion, error) { if d.Device == nil { return nil, errors.New("No Device Found") } request := &u2fhost.AuthenticateRequest{ Challenge: d.ChallengeNonce, Facet: "https://" + d.AppID, AppId: d.AppID, KeyHandle: d.KeyHandle, WebAuthn: true, } // do the change prompted := false timeout := time.After(time.Second * 25) interval := time.NewTicker(time.Millisecond * 250) var responsePayload *SignedAssertion defer func() { d.Device.Close() }() defer interval.Stop() for { select { case <-timeout: return nil, errors.New("Failed to get authentication response after 25 seconds") case <-interval.C: response, err := d.Device.Authenticate(request) if err == nil { responsePayload = &SignedAssertion{ StateToken: d.StateToken, ClientData: response.ClientData, SignatureData: response.SignatureData, AuthenticatorData: response.AuthenticatorData, } fmt.Printf(" ==> Touch accepted. Proceeding with authentication\n") return responsePayload, nil } switch err.(type) { case *u2fhost.TestOfUserPresenceRequiredError: if !prompted { fmt.Printf("\nTouch the flashing U2F device to authenticate...\n") prompted = true } default: return responsePayload, err } } } } // U2FDeviceFinder returns a U2F device type U2FDeviceFinder struct{} func (*U2FDeviceFinder) findDevice() (u2fhost.Device, error) { var err error allDevices := u2fhost.Devices() if len(allDevices) == 0 { return nil, errNoDeviceFound } for i, device := range allDevices { err = device.Open() if err != nil { device.Close() continue } return allDevices[i], nil } return nil, fmt.Errorf("failed to open fido U2F device: %s", err) }