Skip to content
This repository has been archived by the owner on May 18, 2021. It is now read-only.

Commit

Permalink
feat: enable FIDO U2F MFA (#201)
Browse files Browse the repository at this point in the history
adds support for U2F FIDO devices such as yubikey. The U2F device must
already be registered with Okta and be plugged in and available for use.

retry if open fails, there are issues with the low level `hid_open` call
that we rely on in `hidapi`.

These are the libraries we depend on:
https://github.com/marshallbrekka/go-u2fhost/blob/master/hid/wrapper.go
https://github.com/marshallbrekka/go.hid
https://github.com/signal11/hidapi
  • Loading branch information
Aaditya Sondhi authored and nickatsegment committed Sep 4, 2019
1 parent 9787f11 commit 23fa71f
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 2 deletions.
144 changes: 144 additions & 0 deletions lib/mfa/fido.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package mfa

import (
"errors"
"fmt"
"time"

log "github.com/sirupsen/logrus"

u2fhost "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")
)

type FidoClient struct {
ChallengeNonce string
AppId string
Version string
Device u2fhost.Device
KeyHandle string
StateToken string
}

type SignedAssertion struct {
StateToken string `json:"stateToken"`
ClientData string `json:"clientData"`
SignatureData string `json:"signatureData"`
}

func NewFidoClient(challengeNonce, appId, version, keyHandle, stateToken string) (FidoClient, error) {
var device u2fhost.Device
var err error

retryCount := 0
for retryCount < MaxOpenRetries {
device, err = 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)
}

func (d *FidoClient) ChallengeU2f() (*SignedAssertion, error) {

if d.Device == nil {
return nil, errors.New("No Device Found")
}
request := &u2fhost.AuthenticateRequest{
Challenge: d.ChallengeNonce,
// the appid is the only facet.
Facet: d.AppId,
AppId: d.AppId,
KeyHandle: d.KeyHandle,
}
// do the change
prompted := false
timeout := time.After(time.Second * 25)
interval := time.NewTicker(time.Millisecond * 250)
var responsePayload *SignedAssertion

d.Device.Open()

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,
}
fmt.Printf(" ==> Touch accepted. Proceeding with authentication\n")
return responsePayload, nil
}

switch t := err.(type) {
case *u2fhost.TestOfUserPresenceRequiredError:
if !prompted {
fmt.Printf("\nTouch the flashing U2F device to authenticate...\n")
prompted = true
}
default:
log.Debug("Got ErrType: ", t)
return responsePayload, err
}
}
}

return responsePayload, nil
}

func 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 {
log.Debugf("failed to open device: %s", err)
device.Close()

continue
}

return allDevices[i], nil
}

return nil, fmt.Errorf("failed to open fido U2F device: %s", err)
}
34 changes: 32 additions & 2 deletions lib/okta.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/segmentio/aws-okta/lib/mfa"
"github.com/segmentio/aws-okta/lib/saml"
log "github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -312,6 +313,7 @@ func (o *OktaClient) selectMFADevice() (*OktaUserAuthnFactor, error) {
func (o *OktaClient) preChallenge(oktaFactorId, oktaFactorType string) ([]byte, error) {
var mfaCode string
var err error

//Software and Hardware based OTP Tokens
if strings.Contains(oktaFactorType, "token") {
log.Debug("Token MFA")
Expand Down Expand Up @@ -340,6 +342,7 @@ func (o *OktaClient) preChallenge(oktaFactorId, oktaFactorType string) ([]byte,
return nil, err
}
}

payload, err := json.Marshal(OktaStateToken{
StateToken: o.UserAuth.StateToken,
PassCode: mfaCode,
Expand Down Expand Up @@ -380,8 +383,34 @@ func (o *OktaClient) postChallenge(payload []byte, oktaFactorProvider string, ok
}
}()
}
}
} else if oktaFactorProvider == "FIDO" {
f := o.UserAuth.Embedded.Factor

log.Debug("FIDO U2F Details:")
log.Debug(" ChallengeNonce: ", f.Embedded.Challenge.Nonce)
log.Debug(" AppId: ", f.Profile.AppId)
log.Debug(" CredentialId: ", f.Profile.CredentialId)
log.Debug(" StateToken: ", o.UserAuth.StateToken)

fidoClient, err := mfa.NewFidoClient(f.Embedded.Challenge.Nonce,
f.Profile.AppId,
f.Profile.Version,
f.Profile.CredentialId,
o.UserAuth.StateToken)
if err != nil {
return err
}

signedAssertion, err := fidoClient.ChallengeU2f()
if err != nil {
return err
}
// re-assign the payload to provide U2F responses.
payload, err = json.Marshal(signedAssertion)
if err != nil {
return err
}
}
// Poll Okta until authentication has been completed
for o.UserAuth.Status != "SUCCESS" {
select {
Expand Down Expand Up @@ -410,7 +439,6 @@ func (o *OktaClient) challengeMFA() (err error) {
var payload []byte
var oktaFactorType string

log.Debugf("%s", o.UserAuth.StateToken)
factor, err := o.selectMFADevice()
if err != nil {
log.Debug("Failed to select MFA device")
Expand Down Expand Up @@ -465,6 +493,8 @@ func GetFactorId(f *OktaUserAuthnFactor) (id string, err error) {
id = f.Id
case "sms":
id = f.Id
case "u2f":
id = f.Id
case "push":
if f.Provider == "OKTA" || f.Provider == "DUO" {
id = f.Id
Expand Down
12 changes: 12 additions & 0 deletions lib/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ type OktaUserAuthnFactor struct {
FactorType string `json:"factorType"`
Provider string `json:"provider"`
Embedded OktaUserAuthnFactorEmbedded `json:"_embedded"`
Profile OktaUserAuthnFactorProfile `json:"profile"`
}

type OktaUserAuthnFactorProfile struct {
CredentialId string `json:"credentialId"`
AppId string `json:"appId"`
Version string `json:"version"`
}

type OktaUserAuthnFactorEmbedded struct {
Verification OktaUserAuthnFactorEmbeddedVerification `json:"verification"`
Challenge OktaUserAuthnFactorEmbeddedChallenge `json:"challenge"`
}

type OktaUserAuthnFactorEmbeddedVerification struct {
Expand All @@ -43,6 +51,10 @@ type OktaUserAuthnFactorEmbeddedVerification struct {
Links OktaUserAuthnFactorEmbeddedVerificationLinks `json:"_links"`
}

type OktaUserAuthnFactorEmbeddedChallenge struct {
Nonce string `json:"nonce"`
TimeoutSeconnds int `json:"timeoutSeconds"`
}
type OktaUserAuthnFactorEmbeddedVerificationLinks struct {
Complete OktaUserAuthnFactorEmbeddedVerificationLinksComplete `json:"complete"`
}
Expand Down

0 comments on commit 23fa71f

Please sign in to comment.