Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: support previously registered U2F factors as WebAuthN factors. #8

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions api/factors.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func (f *Factor) UnmarshalJSON(data []byte) error {
profile := FactorProfileU2F{}
err = json.Unmarshal([]byte(factor.Profile), &profile)
f.Profile = profile
case factors.FactorTypeWebAuthN:
profile := FactorProfileWebAuthN{}
err = json.Unmarshal([]byte(factor.Profile), &profile)
f.Profile = profile
default:
// Ignore any profile contents we don't understand
return nil
Expand All @@ -92,6 +96,7 @@ type FactorEmbedded struct {
}

type Challenge struct {
Challenge string
Nonce string
TimeoutSeconds int
}
Expand Down Expand Up @@ -121,6 +126,10 @@ type FactorProfileU2F struct {
Version string `json:"version,omitempty"`
}

type FactorProfileWebAuthN struct {
CredentialId string `json:"credentialId,omitempty"`
}

type FactorVerify struct {
StateToken string `json:"stateToken"`
}
Expand All @@ -141,6 +150,13 @@ type FactorVerifyPush struct {
FactorVerify
}

type FactorVerifyWebAuthN struct {
FactorVerify
ClientData string `json:"clientData"`
SignatureData string `json:"signatureData"`
AuthenticatorData string `json:"authenticatorData"`
}

func indexOfFactorType(factorType factors.FactorType) int {
for i, t := range knownFactors {
if factorType == t {
Expand All @@ -152,6 +168,7 @@ func indexOfFactorType(factorType factors.FactorType) int {

var knownFactors = []factors.FactorType{
factors.FactorTypeU2F,
factors.FactorTypeWebAuthN,
factors.FactorTypeToken,
factors.FactorTypeTokenSoftwareTOTP,
factors.FactorTypeTokenHardware,
Expand Down
38 changes: 38 additions & 0 deletions api/factors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ func TestFactorUnmarshalJSON(t *testing.T) {
},
},
},
{
input: sampleWebAuthNFactor,
expected: Factor{
Id: "fufb6rh45mUxtEJz61t7",
FactorType: factors.FactorTypeWebAuthN,
Provider: "FIDO",
Profile: FactorProfileWebAuthN{
CredentialId: "s94CdJnUd148p95PNq7AaY2Dv1QFrLJ12Vpkno-Q7WalmBTtB5TMnzDNL_yX84Ay49qnEiUXtSx0KK5I60ht2g",
},
Links: Links{
Verify: Link{
HREF: "https://example.okta.com/api/v1/authn/factors/fufb6rh45mUxtEJz61t7/verify",
},
},
},
},
}

for i, testCase := range testCases {
Expand Down Expand Up @@ -107,3 +123,25 @@ var sampleU2FFactor = `
}
}
`

var sampleWebAuthNFactor = `
{
"id": "fufb6rh45mUxtEJz61t7",
"factorType": "webauthn",
"provider": "FIDO",
"vendorName": "FIDO",
"profile": {
"credentialId": "s94CdJnUd148p95PNq7AaY2Dv1QFrLJ12Vpkno-Q7WalmBTtB5TMnzDNL_yX84Ay49qnEiUXtSx0KK5I60ht2g"
},
"_links": {
"verify": {
"href": "https://example.okta.com/api/v1/authn/factors/fufb6rh45mUxtEJz61t7/verify",
"hints": {
"allow": [
"POST"
]
}
}
}
}
`
62 changes: 62 additions & 0 deletions authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ by the handleAuthUserFlow function.
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -117,6 +119,11 @@ func (c *OktaClient) handleMFARequired(transaction api.AuthenticationTransaction
c.prompts.CheckU2FPresence(u2fProfileToChallenge(c.domain, "", factor.Profile.(api.FactorProfileU2F))) {
return c.startMFA(transaction, factor)
}

if factor.FactorType == factors.FactorTypeWebAuthN && autoAttemptU2F &&
c.prompts.CheckU2FPresence(webAuthNProfileToChallenge(c.domain, "", factor.Profile.(api.FactorProfileWebAuthN))) {
return c.startMFA(transaction, factor)
}
}

publicFactors := apiFactorsToPublicFactors(supported)
Expand Down Expand Up @@ -155,6 +162,52 @@ func (c *OktaClient) handleMFAChallenge(transaction api.AuthenticationTransactio
case factors.FactorTypeU2F:
return c.handleFactorTypeU2F(transaction)

case factors.FactorTypeWebAuthN:
profile, ok := transaction.Embedded.Factor.Profile.(api.FactorProfileWebAuthN)
if !ok {
c.log("Profile was not of type FactorProfileWebAuthN: %s", transaction.Embedded.Factor.Profile)
return c.cancelCurrentFactorWithErrorMessage(transaction, unexpectedErrorMessage)
}

// Setup a context with the timeout set to the value provided by Okta
timeoutSeconds := 30
ctx, _ := context.WithTimeout(context.Background(), time.Second*time.Duration(timeoutSeconds))

authResp, err := c.prompts.VerifyU2F(ctx, VerifyU2FRequest{
Copy link

@alsmola alsmola Dec 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is VerifyU2FRequest implemented? I'm trying to get webauthn with Okta to work in Go with code very similar to segmentio/aws-okta#201 using https://github.com/marshallbrekka/go-u2fhost under the hood. I'm getting a "The provided key handle is not present on the device, or was created with a different application parameter" error.

If you have a Go implementation of U2F that works with the new Okta webauthn factors, I'd love to know about it!

Facet: c.domain,
AppId: c.domain,
KeyHandle: profile.CredentialId,
Challenge: transaction.Embedded.Factor.Embedded.Challenge.Challenge,
})
if err != nil {
c.prompts.PresentUserError(fmt.Sprintf("Failed to authenticate: %s\n", err))
return c.cancelCurrentFactor(transaction)
}

authData := make([]byte, 37)
rpid := sha256.Sum256([]byte(c.domain))
copy(authData[0:32], rpid[0:32])
authData[32] = 0x80
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When logging into Okta through a browser, this is set to 0x05, which sets the user verified and user present flags.

https://w3c.github.io/webauthn/#authenticator-data


authDataB64 := base64.StdEncoding.EncodeToString(authData)

verifyReq := api.FactorVerifyWebAuthN{
FactorVerify: api.FactorVerify{
StateToken: transaction.StateToken,
},
ClientData: authResp.ClientData,
SignatureData: authResp.SignatureData,
AuthenticatorData: authDataB64,
}
newTransaction, apiError, err := c.sendTransactionRequest(transaction.Links.Next.HREF, &verifyReq)
if err != nil {
return "", err
}
if apiError != nil {
return c.cancelCurrentFactorWithErrorMessage(transaction, apiError.ErrorSummary)
}
return c.handleAuthUserFlow(newTransaction, false)

case factors.FactorTypeTokenSoftwareTOTP, factors.FactorTypeSMS, factors.FactorTypeCall:
return c.handleFactorTypeCode(transaction)

Expand Down Expand Up @@ -395,6 +448,15 @@ func u2fProfileToChallenge(facet, challenge string, profile api.FactorProfileU2F
}
}

func webAuthNProfileToChallenge(facet, challenge string, profile api.FactorProfileWebAuthN) VerifyU2FRequest {
return VerifyU2FRequest{
AppId: facet,
Facet: facet,
KeyHandle: profile.CredentialId,
Challenge: challenge,
}
}

func apiFactorsToPublicFactors(facs []api.Factor) []factors.Factor {
re := make([]factors.Factor, 0, len(facs))

Expand Down
1 change: 1 addition & 0 deletions factors/factors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const (
FactorTypeSMS = FactorType("sms")
FactorTypeCall = FactorType("call")
FactorTypeU2F = FactorType("u2f")
FactorTypeWebAuthN = FactorType("webauthn")
FactorTypeToken = FactorType("token")
FactorTypeTokenSoftwareTOTP = FactorType("token:software:totp")
FactorTypeTokenHardware = FactorType("token:hardware")
Expand Down