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

Pass a list of allowed origins instead of a single origin #184

Merged
merged 16 commits into from
Apr 2, 2024
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
### 0.10.0.0

* [#183](https://github.com/tweag/webauthn/pull/183) Pass a list of allowed origins instead of a single origin.
arianvp marked this conversation as resolved.
Show resolved Hide resolved
This is a breaking change needed for allowing native apps to use WebAuthn. It is also needed for Relying Parties
that want to allow multiple subdomains to access WebAuthn credentials.
Unlike the rest of this library, which strictly follows the L2 version of this spec, this feature is defined
in the [L3 draft](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin). However because WebAuthn on
Native Apps is widely deployed through the push of Passkeys we decided to include this feature in this library early.


### 0.9.0.0

* [#182](https://github.com/tweag/webauthn/pull/182) Migrate to the crypton library ecosystem.
Expand Down
5 changes: 3 additions & 2 deletions server/src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import System.Hourglass (dateCurrent)
import qualified Web.Cookie as Cookie
import Web.Scotty (ScottyM)
import qualified Web.Scotty as Scotty
import qualified Data.List.NonEmpty as NE

data RegisterBeginReq = RegisterBeginReq
{ accountName :: Text,
Expand Down Expand Up @@ -261,7 +262,7 @@ completeRegistration origin rpIdHash db pending registryVar = do
-- FIXME
registry <- Scotty.liftAndCatchIO $ readTVarIO registryVar
now <- Scotty.liftAndCatchIO dateCurrent
result <- case WA.verifyRegistrationResponse origin rpIdHash registry now options cred of
result <- case WA.verifyRegistrationResponse (NE.singleton origin) rpIdHash registry now options cred of
Failure errs@(err :| _) -> do
Scotty.liftAndCatchIO $ TIO.putStrLn $ "Register complete had errors: " <> Text.pack (show errs)
fail $ show err
Expand Down Expand Up @@ -376,7 +377,7 @@ completeLogin origin rpIdHash db pending = do
-- not be verified.
let verificationResult =
WA.verifyAuthenticationResponse
origin
(NE.singleton origin)
rpIdHash
(Just (WA.ceUserHandle entry))
entry
Expand Down
36 changes: 30 additions & 6 deletions src/Crypto/WebAuthn/Operation/Authentication.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import qualified Data.ByteString.Lazy as LBS
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import Data.Validation (Validation)
import qualified Data.List.NonEmpty as NE

-- | Errors that may occur during [assertion](https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion)
data AuthenticationError
Expand Down Expand Up @@ -78,7 +79,7 @@ data AuthenticationError
AuthenticationOriginMismatch
{ -- | The origin explicitly passed to the `verifyAuthenticationResponse`
-- response, set by the RP
aeExpectedOrigin :: M.Origin,
aeExpectedOrigin :: NonEmpty M.Origin,
-- | The origin received from the client as part of the client data
aeReceivedOrigin :: M.Origin
}
Expand Down Expand Up @@ -157,11 +158,32 @@ newtype AuthenticationResult = AuthenticationResult

-- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion)
-- Verifies a 'M.Credential' response for an [authentication ceremony](https://www.w3.org/TR/webauthn-2/#authentication).
--
-- The 'arSignatureCounterResult' field of the result should be inspected to
-- enforce Relying Party policy regarding potentially cloned authenticators.
--
-- Though this library implements the WebAuthn L2 spec, for origin validation we
-- follow the L3 draft. This is because allowing multiple origins is often
-- needed in the wild. See [Validating the origin of a credential](https://www.w3.org/tr/webauthn-3/#sctn-validating-origin)
-- more details.
-- In the simplest case, just a single origin is allowed and this is the 'M.RpId' with @https://@ prepended:
--
-- > verifyAuthenticationResponse (NE.singleton (M.Origin "https://example.org")) ...
--
-- In the more complex case, multiple origins are allowed:
--
-- > verifyAuthenticationResponse (M.Origin <$> "https://example.org" :| ["https://signin.example.org"]) ...
--
-- One might also allow native apps to authenticate:
--
-- > verifyAuthenticationResponse (M.Origin <$> "https://example.org" :| ["ios:bundle-id:org.example.ourapp"]) ...
--
-- See Apple's documentation on [associated domains](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys/)
-- and Google's documentation on [Digital Asset Links](https://developers.google.com/identity/passkeys/developer-guides) for more information on how to link app
-- origins to your Relying Party ID.
verifyAuthenticationResponse ::
-- | The origin of the server
M.Origin ->
-- | The list of allowed origins for the ceremony
NonEmpty M.Origin ->
-- | The hash of the relying party id
M.RpIdHash ->
-- | The user handle, in case the user is identified already
Expand All @@ -179,7 +201,7 @@ verifyAuthenticationResponse ::
-- Or in case of success a signature counter result, which should be dealt
-- with
Validation (NonEmpty AuthenticationError) AuthenticationResult
verifyAuthenticationResponse origin rpIdHash midentifiedUser entry options credential = do
verifyAuthenticationResponse origins rpIdHash midentifiedUser entry options credential = do
-- 1. Let options be a new PublicKeyCredentialRequestOptions structure
-- configured to the Relying Party's needs for the ceremony.
-- NOTE: Implemented by caller
Expand Down Expand Up @@ -290,9 +312,11 @@ verifyAuthenticationResponse origin rpIdHash midentifiedUser entry options crede
AuthenticationChallengeMismatch (M.coaChallenge options) (M.ccdChallenge c)

-- 13. Verify that the value of C.origin matches the Relying Party's origin.
unless (M.ccdOrigin c == origin) $
-- NOTE: We follow the L3 draft of the spec here, which allows for multiple origins.
-- https://www.w3.org/TR/webauthn-3/#rp-op-verifying-assertion-step-origin
unless (M.ccdOrigin c `elem` NE.toList origins) $
failure $
AuthenticationOriginMismatch origin (M.ccdOrigin c)
AuthenticationOriginMismatch origins (M.ccdOrigin c)

-- 14. Verify that the value of C.tokenBinding.status matches the state of
-- Token Binding for the TLS connection over which the attestation was
Expand Down
36 changes: 30 additions & 6 deletions src/Crypto/WebAuthn/Operation/Registration.hs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ data RegistrationError
RegistrationOriginMismatch
{ -- | The origin explicitly passed to the `verifyRegistrationResponse`
-- response, set by the RP
reExpectedOrigin :: M.Origin,
reExpectedOrigin :: NonEmpty M.Origin,
-- | The origin received from the client as part of the client data
reReceivedOrigin :: M.Origin
}
Expand Down Expand Up @@ -264,13 +264,35 @@ data RegistrationResult = RegistrationResult
deriving instance ToJSON RegistrationResult

-- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential)
-- Verifies a 'M.Credential' response for a [registration ceremony](https://www.w3.org/TR/webauthn-2/#registration-ceremony).
--
-- The resulting 'rrEntry' of this call should be stored in a database by the
-- Relying Party. The 'rrAttestationStatement' contains the result of the
-- attempted attestation, allowing the Relying Party to reject certain
-- authenticators/attempted entry creations based on policy.
--
-- Though this library implements the WebAuthn L2 spec, for origin validation we
-- follow the L3 draft. This is because allowing multiple origins is often
-- needed in the wild. See [Validating the origin of a credential](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin)
-- more details.
-- In the simplest case, just a single origin is allowed and this is the 'M.RpId' with @https://@ prepended:
--
-- > verifyRegistrationResponse (NE.singleton (M.Origin "https://example.org")) ...
--
-- In the more complex case, multiple origins are allowed:
--
-- > verifyRegistrationResponse (M.Origin <$> "https://example.org" :| ["https://signin.example.org"]) ...
--
-- One might also allow native apps to authenticate:
--
-- > verifyRegistrationResponse (M.Origin <$> "https://example.org" :| ["ios:bundle-id:org.example.ourapp"]) ...
--
-- See Apple's documentation on [associated domains](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys/)
-- and Google's documentation on [Digital Asset Links](https://developers.google.com/identity/passkeys/developer-guides) for more information on how to link app
-- origins to your Relying Party ID.
verifyRegistrationResponse ::
-- | The origin of the server
M.Origin ->
-- | The list of allowed origins for the ceremony
NonEmpty M.Origin ->
-- | The relying party id
M.RpIdHash ->
-- | The metadata registry, used for verifying the validity of the
Expand All @@ -287,7 +309,7 @@ verifyRegistrationResponse ::
-- Or () in case of a result.
Validation (NonEmpty RegistrationError) RegistrationResult
verifyRegistrationResponse
rpOrigin
origins
rpIdHash
registry
currentTime
Expand Down Expand Up @@ -349,9 +371,11 @@ verifyRegistrationResponse
RegistrationChallengeMismatch corChallenge (M.ccdChallenge c)

-- 9. Verify that the value of C.origin matches the Relying Party's origin.
unless (rpOrigin == M.ccdOrigin c) $
-- NOTE: We follow the L3 draft of the spec here, which allows for multiple origins.
-- https://www.w3.org/TR/webauthn-3/#rp-op-registering-a-new-credential-step-origin
unless (M.ccdOrigin c `elem` NE.toList origins) $
failure $
RegistrationOriginMismatch rpOrigin (M.ccdOrigin c)
RegistrationOriginMismatch origins (M.ccdOrigin c)

-- 10. Verify that the value of C.tokenBinding.status matches the state of
-- Token Binding for the TLS connection over which the assertion was
Expand Down
4 changes: 2 additions & 2 deletions tests/Emulation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ register ao conformance authenticator registry now = do
let registerResult =
toEither $
O.verifyRegistrationResponse
(aoOrigin ao)
(NE.singleton $ aoOrigin ao)
(M.RpIdHash . hash . encodeUtf8 . M.unRpId $ aoRpId ao)
registry
now
Expand All @@ -109,7 +109,7 @@ login ao conformance authenticator [email protected] {..} = do
. second O.arSignatureCounterResult
. toEither
$ O.verifyAuthenticationResponse
(aoOrigin ao)
(NE.singleton (aoOrigin ao))
(M.RpIdHash . hash . encodeUtf8 . M.unRpId $ aoRpId ao)
(Just ceUserHandle)
ce
Expand Down
10 changes: 5 additions & 5 deletions tests/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ registerTestFromFile fp origin rpId verifiable service now = do
let registerResult =
toEither $
O.verifyRegistrationResponse
origin
(NE.singleton origin)
(M.RpIdHash . hash . encodeUtf8 . M.unRpId $ rpId)
service
now
Expand Down Expand Up @@ -125,7 +125,7 @@ main = Hspec.hspec $ do
registerResult =
toEither $
O.verifyRegistrationResponse
(M.Origin "http://localhost:8080")
(NE.singleton $ M.Origin "http://localhost:8080")
(M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))
registry
predeterminedDateTime
Expand All @@ -142,7 +142,7 @@ main = Hspec.hspec $ do
signInResult =
toEither $
O.verifyAuthenticationResponse
(M.Origin "http://localhost:8080")
(NE.singleton $ M.Origin "http://localhost:8080")
(M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))
(Just (M.UserHandle "UserId"))
credentialEntry
Expand All @@ -163,7 +163,7 @@ main = Hspec.hspec $ do
registerResult =
toEither $
O.verifyRegistrationResponse
(M.Origin "http://localhost:8080")
(NE.singleton $ M.Origin "http://localhost:8080")
(M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))
registry
predeterminedDateTime
Expand All @@ -180,7 +180,7 @@ main = Hspec.hspec $ do
signInResult =
toEither $
O.verifyAuthenticationResponse
(M.Origin "http://localhost:8080")
(NE.singleton $ M.Origin "http://localhost:8080")
(M.RpIdHash . hash $ ("localhost" :: ByteString.ByteString))
(Just (M.UserHandle "UserId"))
credentialEntry
Expand Down
Loading