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

feat: enable FIDO U2F MFA #201

Merged
merged 1 commit into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -297,6 +298,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 @@ -325,6 +327,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 @@ -365,8 +368,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 @@ -395,7 +424,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 @@ -444,6 +472,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