From 7f969a5be3daf788bf1202266e2b9d3235eee2e4 Mon Sep 17 00:00:00 2001 From: csalas Date: Tue, 26 Apr 2022 17:00:39 -0500 Subject: [PATCH 1/6] MDS integrations added to both backend and client --- .../CreateAuth/CreateAuthChallengeFIDO2.js | 12 +- .../FIDO2KitAPI/FIDO2KitAPI.js | 16 +- .../src/main/java/com/yubicolabs/App.java | 190 ++-- .../data/AttestationRegistration.java | 29 + .../data/CredentialRegistration.java | 6 +- backend/template.yaml | 913 +++++++++--------- clients/web/react/public/i18n/en-US.json | 6 +- .../RegisterPage/RegisterKeySuccessStep.tsx | 2 +- .../_components/Credential/AddCredential.tsx | 39 +- .../src/_components/Credential/Credential.tsx | 3 +- .../_components/Credential/EditCredential.tsx | 70 +- .../TrustedDevices/AddTrustedDevice.tsx | 7 +- .../TrustedDevices/TrustedDevice.tsx | 23 +- .../react/src/_components/WebAuthnClient.ts | 5 +- .../src/_components/component.module.css | 5 +- 15 files changed, 730 insertions(+), 596 deletions(-) create mode 100644 backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/AttestationRegistration.java diff --git a/backend/lambda-functions/CreateAuth/CreateAuthChallengeFIDO2.js b/backend/lambda-functions/CreateAuth/CreateAuthChallengeFIDO2.js index f147d51..26fddc7 100644 --- a/backend/lambda-functions/CreateAuth/CreateAuthChallengeFIDO2.js +++ b/backend/lambda-functions/CreateAuth/CreateAuthChallengeFIDO2.js @@ -131,9 +131,9 @@ async function getCreateCredentialsOptions(event, creds) { const coseLookup = {"ES256": -7, "EdDSA": -8, "RS256": -257}; - startRegisterPayload.requestId = startRegisterPayload.requestId.base64; - startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64; - startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64; + startRegisterPayload.requestId = startRegisterPayload.requestId.base64url; + startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64url; + startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64url; startRegisterPayload.publicKeyCredentialCreationOptions.attestation = startRegisterPayload.publicKeyCredentialCreationOptions.attestation.toLowerCase(); startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification = startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification.toLowerCase(); startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.authenticatorAttachment = authSelectorResolve[startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.authenticatorAttachment]; @@ -179,14 +179,14 @@ async function getCredentialsOptions(username) { let startAuthPayload = JSON.parse(JSON.parse(response.Payload)); console.log("startAuthPayload: ", startAuthPayload); - startAuthPayload.requestId = startAuthPayload.requestId.base64; + startAuthPayload.requestId = startAuthPayload.requestId.base64url; console.log("requestId: ", startAuthPayload.requestId); startAuthPayload.publicKeyCredentialRequestOptions.userVerification = startAuthPayload.publicKeyCredentialRequestOptions.userVerification.toLowerCase(); - startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64; + startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64url; console.log("challenge: ", startAuthPayload.publicKeyCredentialRequestOptions.challenge); startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials = startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials.map( (cred) => { cred.type = cred.type.toLowerCase().replace('_','-'); - cred.id = cred.id.base64; + cred.id = cred.id.base64url; return cred }); console.log("response payload: ", startAuthPayload); diff --git a/backend/lambda-functions/FIDO2KitAPI/FIDO2KitAPI.js b/backend/lambda-functions/FIDO2KitAPI/FIDO2KitAPI.js index b1a4a7e..165cb9a 100644 --- a/backend/lambda-functions/FIDO2KitAPI/FIDO2KitAPI.js +++ b/backend/lambda-functions/FIDO2KitAPI/FIDO2KitAPI.js @@ -180,7 +180,7 @@ async function updateFIDO2CredentialNickname(username, body) { const payload = JSON.stringify({ "type": "updateCredentialNickname", "username": username, - "credentialId": data.credential.credentialId.base64, + "credentialId": data.credential.credentialId.base64url, "nickname": data.credentialNickname.value, }); console.log("updateCredentialNickname request payload: "+payload); @@ -264,15 +264,15 @@ async function startUsernamelessAuthentication() { let startAuthPayload = JSON.parse(JSON.parse(response.Payload)); console.log("startAuthPayload: ", startAuthPayload); - startAuthPayload.requestId = startAuthPayload.requestId.base64; + startAuthPayload.requestId = startAuthPayload.requestId.base64url; console.log("requestId: ", startAuthPayload.requestId); startAuthPayload.publicKeyCredentialRequestOptions.userVerification = startAuthPayload.publicKeyCredentialRequestOptions.userVerification.toLowerCase(); - startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64; + startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64url; console.log("challenge: ", startAuthPayload.publicKeyCredentialRequestOptions.challenge); if(startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials){ startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials = startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials.map( (cred) => { cred.type = cred.type.toLowerCase().replace('_','-'); - cred.id = cred.id.base64; + cred.id = cred.id.url; return cred }); } @@ -322,9 +322,9 @@ async function startRegisterFIDO2Credential(profile, body, uid) { const coseLookup = {"ES256": -7, "EdDSA": -8, "RS256": -257}; - startRegisterPayload.requestId = startRegisterPayload.requestId.base64; - startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64; - startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64; + startRegisterPayload.requestId = startRegisterPayload.requestId.base64url; + startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64url; + startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64url; startRegisterPayload.publicKeyCredentialCreationOptions.attestation = startRegisterPayload.publicKeyCredentialCreationOptions.attestation.toLowerCase(); startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification = startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification.toLowerCase(); startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.residentKey = startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.residentKey.toLowerCase(); @@ -341,7 +341,7 @@ async function startRegisterFIDO2Credential(profile, body, uid) { }); startRegisterPayload.publicKeyCredentialCreationOptions.excludeCredentials = startRegisterPayload.publicKeyCredentialCreationOptions.excludeCredentials.map( (cred) => { cred.type = cred.type.toLowerCase().replace('_','-'); - cred.id = cred.id.base64; + cred.id = cred.id.base64url; console.log("cred: "+ JSON.stringify(cred)); return cred; }); diff --git a/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/App.java b/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/App.java index d6e885f..0fb24c8 100644 --- a/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/App.java +++ b/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/App.java @@ -10,6 +10,13 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.yubico.fido.metadata.AAGUID; +import com.yubico.fido.metadata.AAID; +import com.yubico.fido.metadata.AttachmentHint; +import com.yubico.fido.metadata.AuthenticatorGetInfo; +import com.yubico.fido.metadata.FidoMetadataDownloader; +import com.yubico.fido.metadata.FidoMetadataService; +import com.yubico.fido.metadata.MetadataBLOB; import com.yubico.internal.util.JacksonCodecs; import com.yubico.webauthn.AssertionResult; import com.yubico.webauthn.FinishAssertionOptions; @@ -19,31 +26,26 @@ import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.StartAssertionOptions; import com.yubico.webauthn.StartRegistrationOptions; -import com.yubico.webauthn.attestation.Attestation; -import com.yubico.webauthn.attestation.AttestationResolver; -import com.yubico.webauthn.attestation.MetadataObject; -import com.yubico.webauthn.attestation.MetadataService; -import com.yubico.webauthn.attestation.StandardMetadataService; -import com.yubico.webauthn.attestation.TrustResolver; -import com.yubico.webauthn.attestation.resolver.CompositeAttestationResolver; -import com.yubico.webauthn.attestation.resolver.CompositeTrustResolver; -import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver; -import com.yubico.webauthn.attestation.resolver.SimpleTrustResolverWithEquality; import com.yubico.webauthn.data.AttestationConveyancePreference; import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; +import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; +import com.yubico.webauthn.data.ResidentKeyRequirement; import com.yubico.webauthn.data.UserIdentity; import com.yubico.webauthn.data.exception.Base64UrlException; import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import com.yubicolabs.data.AssertionRequestWrapper; import com.yubicolabs.data.AssertionResponse; +import com.yubicolabs.data.AttestationRegistration; import com.yubico.webauthn.data.AuthenticatorAttachment; import com.yubicolabs.data.CredentialRegistration; import com.yubicolabs.data.RegistrationRequest; import com.yubicolabs.data.RegistrationResponse; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.cert.PKIXRevocationChecker.Option; import java.io.IOException; import java.io.InputStream; import java.security.SecureRandom; @@ -51,8 +53,19 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.Optional; + +import lombok.extern.java.Log; import lombok.extern.slf4j.Slf4j; +import java.io.File; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.yubico.fido.metadata.MetadataBLOBPayloadEntry; +import com.yubico.fido.metadata.MetadataStatement; /** * Lambda function entry point. You can change to use other pojo type or @@ -81,85 +94,40 @@ public class App implements RequestHandler { private static final String METADATA_PATH = "/metadata.json"; - private final TrustResolver trustResolver = createTrustResolver(); + private final FidoMetadataService mds = initMDS(); - private final MetadataService metadataService = createMetaDataService(); + private FidoMetadataService initMDS() { + try { + MetadataBLOB downloader = FidoMetadataDownloader.builder() + .expectLegalHeader( + "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/") + .useDefaultTrustRoot() + .useTrustRootCacheFile(new File("/tmp/fido-mds-trust-root-cache.bin")) + .useDefaultBlob() + .useBlobCacheFile(new File("/tmp/fido-mds-blob-cache.bin")) + .build() + .loadBlob(); + + FidoMetadataService mds = FidoMetadataService.builder() + .useBlob(downloader) + .build(); + return mds; + } catch (Exception e) { + log.info("Error initializing MDS: {}", gson.toJson(e)); + return null; + } + } private final RelyingParty rp = RelyingParty.builder() .identity(Config.getRpIdentity()) .credentialRepository(this.userStorage) .origins(Config.getOrigins()) .attestationConveyancePreference(Optional.of(AttestationConveyancePreference.DIRECT)) - .metadataService(Optional.of(metadataService)) - .allowUnrequestedExtensions(true) + .attestationTrustSource(mds) .allowUntrustedAttestation(true) .validateSignatureCounter(true) .build(); - private static MetadataObject readMetadata() { - InputStream is = App.class.getResourceAsStream(METADATA_PATH); - try { - return JacksonCodecs.json().readValue(is, MetadataObject.class); - } catch (IOException e) { - log.error("Failed to read metadata from " + METADATA_PATH, e); - throw new RuntimeException(e.getMessage()); - } finally { - Closeables.closeQuietly(is); - } - } - - private TrustResolver createTrustResolver() { - try { - return new CompositeTrustResolver( - Arrays.asList( - StandardMetadataService.createDefaultTrustResolver(), createExtraTrustResolver())); - } catch (CertificateException e) { - log.error("Failed to read trusted certificate(s)", e); - throw new RuntimeException(e.getMessage()); - } - } - - private MetadataService createMetaDataService() { - try { - return new StandardMetadataService( - new CompositeAttestationResolver( - Arrays.asList( - StandardMetadataService.createDefaultAttestationResolver(trustResolver), - createExtraMetadataResolver(trustResolver)))); - } catch (CertificateException e) { - log.error("Failed to read trusted certificate(s)", e); - throw new RuntimeException(e.getMessage()); - } - } - - /** - * Create a {@link TrustResolver} that accepts attestation certificates that are - * directly recognised as trust anchors. - */ - private static TrustResolver createExtraTrustResolver() { - try { - MetadataObject metadata = readMetadata(); - return new SimpleTrustResolverWithEquality(metadata.getParsedTrustedCertificates()); - } catch (CertificateException e) { - log.error("Failed to read trusted certificate(s)", e); - throw new RuntimeException(e.getMessage()); - } - } - - /** - * Create a {@link AttestationResolver} with additional metadata for YubiKey - * devices. - */ - private static AttestationResolver createExtraMetadataResolver(TrustResolver trustResolver) { - try { - MetadataObject metadata = readMetadata(); - return new SimpleAttestationResolver(Collections.singleton(metadata), trustResolver); - } catch (CertificateException e) { - log.error("Failed to read trusted certificate(s)", e); - throw new RuntimeException(e.getMessage()); - } - } - public App() { jsonMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } @@ -255,7 +223,8 @@ Object startRegistration(JsonObject jsonRequest) { StartRegistrationOptions.builder() .user(registrationUserId) .authenticatorSelection(AuthenticatorSelectionCriteria.builder() - .requireResidentKey(requireResidentKey) + .residentKey(requireResidentKey ? ResidentKeyRequirement.REQUIRED + : ResidentKeyRequirement.DISCOURAGED) .authenticatorAttachment( requireAuthenticatorAttachment != null ? requireAuthenticatorAttachment @@ -496,6 +465,7 @@ private CredentialRegistration addRegistration( RegistrationResponse response, RegistrationResult result, RegistrationRequest request) { + Optional attestationMetadata = buildAttestationResult(result); return addRegistration( userIdentity, nickname, @@ -507,7 +477,7 @@ private CredentialRegistration addRegistration( .signatureCount(response.getCredential().getResponse().getParsedAuthenticatorData() .getSignatureCounter()) .build(), - result.getAttestationMetadata(), + attestationMetadata, request); } @@ -516,7 +486,7 @@ private CredentialRegistration addRegistration( Optional nickname, long signatureCount, RegisteredCredential credential, - Optional attestationMetadata, + Optional attestationMetadata, RegistrationRequest request) { CredentialRegistration reg = CredentialRegistration.builder() .userIdentity(userIdentity) @@ -526,8 +496,8 @@ private CredentialRegistration addRegistration( .lastUpdatedTime(clock.instant()) .credential(credential) .signatureCount(signatureCount) - .attestationMetadata(attestationMetadata) .registrationRequest(request) + .attestationMetadata(attestationMetadata) .build(); log.debug( @@ -538,4 +508,60 @@ private CredentialRegistration addRegistration( userStorage.addRegistrationByUsername(userIdentity.getName(), reg); return reg; } + + private Optional buildAttestationResult(RegistrationResult result) { + log.debug("buildAttestationResult() result aaguid: {}", result.getAaguid().getHex()); + + // Find MDS entries based on both the AAGUID and TrustRootCert provided during + // Attestation + Set entries = mds.findEntries(result); + + // If entries is empty, try through only the AAGUID, this allows Windows Hello + // to work + if (entries.size() == 0) { + entries = mds.findEntries(new AAGUID(result.getAaguid())); + } + log.debug("buildAttestationResult() number of entries found for result: {}", entries.size()); + log.debug("buildAttestationResult() entries found for result: {}", gson.toJson(entries)); + + List entriesAaguid = entries.stream() + .filter(ent -> ent.getAaguid().isPresent() + && ent.getAaguid().get().asHexString().equals(result.getAaguid().getHex())) + .collect(Collectors.toList()); + + Optional entriesFinal; + if (entriesAaguid.size() == 0) { + entriesFinal = entries.stream().findAny().flatMap(MetadataBLOBPayloadEntry::getMetadataStatement); + } else { + entriesFinal = entriesAaguid.get(0).getMetadataStatement(); + } + + AttestationRegistration attResult = null; + if (entriesFinal.isPresent()) { + MetadataStatement entryValue = entriesFinal.get(); + Optional aaguid = entryValue.getAaguid(); + Optional aaid = entryValue.getAaid(); + Optional> attachmentHint = entryValue.getAttachmentHint(); + Optional icon = entryValue.getIcon(); + Optional description = entryValue.getDescription(); + Optional> authenticatorTransport; + if (entryValue.getAuthenticatorGetInfo().isPresent()) { + authenticatorTransport = entryValue.getAuthenticatorGetInfo().get().getTransports(); + } else { + authenticatorTransport = Optional.empty(); + } + + attResult = AttestationRegistration.builder() + .aaguid(aaguid.isPresent() ? aaguid.get().asGuidString() : null) + .aaid(aaid.isPresent() ? aaid.get().getValue() : null) + .attachmentHint(attachmentHint.isPresent() ? attachmentHint.get() : null) + .icon(icon.isPresent() ? icon.get() : null) + .description(description.isPresent() ? description.get() : null) + .authenticatorTransport(authenticatorTransport.isPresent() ? authenticatorTransport.get() : null) + .build(); + + log.debug("AttestationRegistration result: {}", attResult); + } + return Optional.ofNullable(attResult); + } } diff --git a/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/AttestationRegistration.java b/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/AttestationRegistration.java new file mode 100644 index 0000000..83480eb --- /dev/null +++ b/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/AttestationRegistration.java @@ -0,0 +1,29 @@ +package com.yubicolabs.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Set; + +import com.yubico.fido.metadata.AttachmentHint; +import com.yubico.webauthn.data.AuthenticatorTransport; + +import lombok.Builder; +import lombok.Value; +import lombok.With; + +@Value +@Builder +@With +public class AttestationRegistration { + @JsonInclude(JsonInclude.Include.NON_NULL) + String aaguid; + @JsonInclude(JsonInclude.Include.NON_NULL) + String aaid; + @JsonInclude(JsonInclude.Include.NON_NULL) + Set attachmentHint; + @JsonInclude(JsonInclude.Include.NON_NULL) + String icon; + @JsonInclude(JsonInclude.Include.NON_NULL) + String description; + @JsonInclude(JsonInclude.Include.NON_NULL) + Set authenticatorTransport; +} diff --git a/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/CredentialRegistration.java b/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/CredentialRegistration.java index c2a12b0..dee7110 100644 --- a/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/CredentialRegistration.java +++ b/backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/CredentialRegistration.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.webauthn.RegisteredCredential; -import com.yubico.webauthn.attestation.Attestation; import com.yubico.webauthn.data.UserIdentity; import java.time.Instant; import java.util.Optional; @@ -13,7 +12,7 @@ @Value @Builder -@With +@With public class CredentialRegistration { long signatureCount; @@ -33,7 +32,7 @@ public class CredentialRegistration { RegisteredCredential credential; - Optional attestationMetadata; + Optional attestationMetadata; RegistrationRequest registrationRequest; @@ -57,4 +56,3 @@ public String getUsername() { } } - diff --git a/backend/template.yaml b/backend/template.yaml index 7f39f35..a4e8ed5 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -1,17 +1,16 @@ AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 -Description: - WebAuthn Starter Kit - Backend Serverless Deployment +Description: WebAuthn Starter Kit - Backend Serverless Deployment Metadata: AWS::ServerlessRepo::Application: Name: yubico-webauthn-starter-kit - Description: - Custom authentication using Amazon Cognito, AWS Lambda, API Gateway, Aurora Serverless (DB), and a YubiKey - Author: 'Yubico AB' - SpdxLicenseId: 'Apache-2.0' - LicenseUrl: '../COPYING' + Description: Custom authentication using Amazon Cognito, AWS Lambda, API Gateway, Aurora Serverless (DB), and a YubiKey + Author: "Yubico AB" + SpdxLicenseId: "Apache-2.0" + LicenseUrl: "../COPYING" ReadmeUrl: README.md - Labels: ['passwordless', 'YubiKey', 'WebAuthn', 'serverless', 'FIDO2', 'Yubico'] + Labels: + ["passwordless", "YubiKey", "WebAuthn", "serverless", "FIDO2", "Yubico"] HomepageUrl: https://github.com/YubicoLabs/WebAuthnKit SemanticVersion: 1.0.0 SourceCodeUrl: https://github.com/YubicoLabs/WebAuthnKit @@ -23,19 +22,19 @@ Globals: Parameters: UserPoolName: Type: String - Default: 'FIDO2UserPool' + Default: "FIDO2UserPool" Description: The name you want to give the Cognito User Pool being created DatabaseName: Type: String - AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' + AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" ConstraintDescription: must begin with a letter and contain only alphanumeric characters - Default: 'fido2database' + Default: "fido2database" Description: The RDS Aurora Serverless Database Name MasterUserName: Type: String MinLength: 8 ConstraintDescription: must be 8 characters or more - Default: 'masterusername' + Default: "masterusername" Description: The RDS Aurora Serverless Master UserName MasterUserPassword: Type: String @@ -43,66 +42,66 @@ Parameters: NoEcho: true DefineAuthChallengeFuncName: Type: String - Default: 'DefineChallenge-FIDO2' + Default: "DefineChallenge-FIDO2" Description: Cognito Define Auth function name CreateAuthChallengeFuncName: Type: String - Default: 'CreateChallenge-FIDO2' + Default: "CreateChallenge-FIDO2" Description: Cognito Create Auth function name VerifyAuthChallengeFuncName: Type: String - Default: 'VerifyChallenge-FIDO2' + Default: "VerifyChallenge-FIDO2" Description: Cognito Verify Auth function name JavaWebAuthnFuncName: Type: String - Default: 'WebAuthn-Java-Lib' + Default: "WebAuthn-Java-Lib" Description: Yubico Java WebAuthn Library WebAuthnKitAPIName: Type: String - Default: 'WebAuthnKitAPI' + Default: "WebAuthnKitAPI" Description: WebAuthnKitAPI API name WebAuthnKitAPIFuncName: Type: String - Default: 'WebAuthnKitAPI' + Default: "WebAuthnKitAPI" Description: WebAuthnKitAPI Lambda function name PreSignUpFuncName: Type: String - Default: 'PreSignUp-FIDO2' + Default: "PreSignUp-FIDO2" Description: Cognito PreSignUp function name CreateDBSchemaFuncName: Type: String - Default: 'CreateFIDO2DBSchema' + Default: "CreateFIDO2DBSchema" Description: Create FIDO2 DB function name CreateDBSchemaCallerFuncName: Type: String - Default: 'CreateFIDO2DBSchemaCaller' + Default: "CreateFIDO2DBSchemaCaller" Description: Calls the DB creation schema function AmplifyHostingAppName: Type: String - Default: 'WebAuthnKit-React-Client' + Default: "WebAuthnKit-React-Client" Description: Hosting App for React Web Client AmplifyHostingBranchName: Type: String - Default: 'dev' + Default: "dev" Description: Hosting Branch for React Web Client APICorsOrigin: Type: String Default: "'*'" APICorsHeaders: Type: String - Default: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" + Default: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" APICorsMethods: Type: String Default: "'DELETE,GET,HEAD,OPTIONS,POST,PUT'" AmplifyDeployType: Type: String - Default: 'manual' + Default: "manual" AllowedValues: [manual] # Used for Amplify Hosting Conditions: isManual: !Equals [!Ref AmplifyDeployType, "manual"] - + Resources: # RDS Aurora Serverless Cluster - Encryption enabled by default # Warning: NOT FREE TIER - Aurora Serverless does not support free tier instance class @@ -113,13 +112,13 @@ Resources: Type: AWS::RDS::DBCluster DeletionPolicy: Delete Properties: - DBClusterIdentifier: + DBClusterIdentifier: Ref: DatabaseName MasterUsername: Ref: MasterUserName MasterUserPassword: Ref: MasterUserPassword - DatabaseName: + DatabaseName: Ref: DatabaseName Engine: aurora EngineMode: serverless @@ -132,114 +131,130 @@ Resources: #SecondsUntilAutoPause: 7200 # Create a SecretsManager to manage the Aurora Serverless credentials RDSAuroraClusterMasterSecret: - Type: 'AWS::SecretsManager::Secret' + Type: "AWS::SecretsManager::Secret" Properties: - Name: + Name: Ref: DatabaseName Description: This contains the RDS Master user credentials for RDS Aurora Serverless Cluster - SecretString: - !Sub | - { - "username": "${MasterUserName}", - "password": "${MasterUserPassword}" - } + SecretString: !Sub | + { + "username": "${MasterUserName}", + "password": "${MasterUserPassword}" + } ############# # IAM Roles # ############# - + # IAM Role and Policy for the CreateDBSchema function - CreateDBSchemaLambdaExecutionRole: + CreateDBSchemaLambdaExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Path: "/" + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - Policies: - - - PolicyName: "LambdaToRDSAuroraServerless" - PolicyDocument: + Policies: + - PolicyName: "LambdaToRDSAuroraServerless" + PolicyDocument: Version: "2012-10-17" - Statement: - - - Effect: "Allow" - Action: + Statement: + - Effect: "Allow" + Action: - "rds-data:DeleteItems" - "rds-data:ExecuteStatement" - "rds-data:GetItems" - "rds-data:InsertItems" - "rds-data:UpdateItems" - Resource: - - !Join ['', ['arn:aws:rds:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':cluster:', !Ref RDSAuroraServerlessCluster]] - - !Join ['', ['arn:aws:rds:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':cluster:', !Ref RDSAuroraServerlessCluster, ':*']] - - - PolicyName: "LambdaToAWSSecrets" - PolicyDocument: + Resource: + - !Join [ + "", + [ + "arn:aws:rds:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":cluster:", + !Ref RDSAuroraServerlessCluster, + ], + ] + - !Join [ + "", + [ + "arn:aws:rds:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":cluster:", + !Ref RDSAuroraServerlessCluster, + ":*", + ], + ] + - PolicyName: "LambdaToAWSSecrets" + PolicyDocument: Version: "2012-10-17" - Statement: - - - Effect: "Allow" + Statement: + - Effect: "Allow" Action: "secretsmanager:GetSecretValue" - Resource: - - !Join ['', [!Ref RDSAuroraClusterMasterSecret]] - - !Join ['', [!Ref RDSAuroraClusterMasterSecret, ':*']] + Resource: + - !Join ["", [!Ref RDSAuroraClusterMasterSecret]] + - !Join ["", [!Ref RDSAuroraClusterMasterSecret, ":*"]] # IAM Role and Policy for the JavaWebAuthnLib function - JavaLibLambdaExecutionRole: + JavaLibLambdaExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" # IAM Role and Policy for the FIDO2 API function - APILambdaExecutionRole: + APILambdaExecutionRole: Type: "AWS::IAM::Role" DependsOn: - - JavaWebAuthnFunction + - JavaWebAuthnFunction Properties: AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" # IAM Role and Policy for the CreateAuth and VerifyAuth functions - CreateVerifyLambdaExecutionRole: + CreateVerifyLambdaExecutionRole: Type: "AWS::IAM::Role" DependsOn: - - JavaWebAuthnFunction + - JavaWebAuthnFunction Properties: AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" @@ -251,17 +266,26 @@ Resources: LambdaToWebAuthnLibPolicy: Type: "AWS::IAM::Policy" DependsOn: - - JavaWebAuthnFunction + - JavaWebAuthnFunction Properties: PolicyName: LambdaToWebAuthnLibPolicy PolicyDocument: Version: 2012-10-17 - Statement: - - - Effect: "Allow" + Statement: + - Effect: "Allow" Action: "lambda:InvokeFunction" Resource: - - !Join ['', ['arn:aws:lambda:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':function:', !Ref JavaWebAuthnFuncName]] + - !Join [ + "", + [ + "arn:aws:lambda:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":function:", + !Ref JavaWebAuthnFuncName, + ], + ] Roles: - !Ref CreateVerifyLambdaExecutionRole - !Ref APILambdaExecutionRole @@ -270,23 +294,43 @@ Resources: RDSAuroraServerlessPolicy: Type: "AWS::IAM::Policy" DependsOn: - - RDSAuroraServerlessCluster + - RDSAuroraServerlessCluster Properties: PolicyName: RDSAuroraServerlessPolicy PolicyDocument: Version: 2012-10-17 - Statement: - - - Effect: "Allow" - Action: + Statement: + - Effect: "Allow" + Action: - "rds-data:DeleteItems" - "rds-data:ExecuteStatement" - "rds-data:GetItems" - "rds-data:InsertItems" - "rds-data:UpdateItems" - Resource: - - !Join ['', ['arn:aws:rds:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':cluster:', !Ref RDSAuroraServerlessCluster]] - - !Join ['', ['arn:aws:rds:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':cluster:', !Ref RDSAuroraServerlessCluster, ':*']] + Resource: + - !Join [ + "", + [ + "arn:aws:rds:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":cluster:", + !Ref RDSAuroraServerlessCluster, + ], + ] + - !Join [ + "", + [ + "arn:aws:rds:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":cluster:", + !Ref RDSAuroraServerlessCluster, + ":*", + ], + ] Roles: - !Ref JavaLibLambdaExecutionRole - !Ref APILambdaExecutionRole @@ -296,18 +340,17 @@ Resources: AWSSecretsPolicy: Type: "AWS::IAM::Policy" DependsOn: - - RDSAuroraClusterMasterSecret + - RDSAuroraClusterMasterSecret Properties: PolicyName: AWSSecretsPolicy PolicyDocument: Version: 2012-10-17 - Statement: - - - Effect: "Allow" + Statement: + - Effect: "Allow" Action: "secretsmanager:GetSecretValue" - Resource: - - !Join ['', [!Ref RDSAuroraClusterMasterSecret]] - - !Join ['', [!Ref RDSAuroraClusterMasterSecret, ':*']] + Resource: + - !Join ["", [!Ref RDSAuroraClusterMasterSecret]] + - !Join ["", [!Ref RDSAuroraClusterMasterSecret, ":*"]] Roles: - !Ref JavaLibLambdaExecutionRole - !Ref APILambdaExecutionRole @@ -320,12 +363,21 @@ Resources: PolicyName: CognitoAdminPolicy PolicyDocument: Version: 2012-10-17 - Statement: - - - Effect: "Allow" + Statement: + - Effect: "Allow" Action: "cognito-idp:AdminUpdateUserAttributes" - Resource: - - !Join ['', ['arn:aws:cognito-idp:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':userpool/', !Ref UserPool]] + Resource: + - !Join [ + "", + [ + "arn:aws:cognito-idp:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":userpool/", + !Ref UserPool, + ], + ] Roles: - !Ref CreateVerifyLambdaExecutionRole @@ -333,7 +385,7 @@ Resources: DefineAuthChallenge: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: DefineAuthChallengeFuncName CodeUri: lambda-functions/DefineAuth/ Handler: DefineAuthChallengeFIDO2.handler @@ -342,7 +394,7 @@ Resources: CreateAuthChallenge: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: CreateAuthChallengeFuncName CodeUri: lambda-functions/CreateAuth/ Handler: CreateAuthChallengeFIDO2.handler @@ -351,23 +403,21 @@ Resources: Environment: Variables: DatabaseName: !Ref DatabaseName - DBAuroraClusterArn: - !Join - - '' - - - - !Sub 'arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:' - - !Ref RDSAuroraServerlessCluster - DBSecretsStoreArn: !Join ['', [!Ref RDSAuroraClusterMasterSecret]] + DBAuroraClusterArn: !Join + - "" + - - !Sub "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:" + - !Ref RDSAuroraServerlessCluster + DBSecretsStoreArn: !Join ["", [!Ref RDSAuroraClusterMasterSecret]] WebAuthnLibFunction: !Ref JavaWebAuthnFuncName - Role: - Fn::GetAtt: + Role: + Fn::GetAtt: - "CreateVerifyLambdaExecutionRole" - "Arn" # Verify Auth Challenge - Function trigger VerifyAuthChallengeResponse: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: VerifyAuthChallengeFuncName CodeUri: lambda-functions/VerifyAuth/ Handler: VerifyAuthChallengeFIDO2.handler @@ -376,16 +426,14 @@ Resources: Environment: Variables: DatabaseName: !Ref DatabaseName - DBAuroraClusterArn: - !Join - - '' - - - - !Sub 'arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:' - - !Ref RDSAuroraServerlessCluster - DBSecretsStoreArn: !Join ['', [!Ref RDSAuroraClusterMasterSecret]] + DBAuroraClusterArn: !Join + - "" + - - !Sub "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:" + - !Ref RDSAuroraServerlessCluster + DBSecretsStoreArn: !Join ["", [!Ref RDSAuroraClusterMasterSecret]] WebAuthnLibFunction: !Ref JavaWebAuthnFuncName - Role: - Fn::GetAtt: + Role: + Fn::GetAtt: - "CreateVerifyLambdaExecutionRole" - "Arn" @@ -393,7 +441,7 @@ Resources: PreSignUp: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: PreSignUpFuncName CodeUri: lambda-functions/PreSignUp/ Handler: PreSignUpFIDO2.handler @@ -403,7 +451,7 @@ Resources: WebAuthnKitAPIFunction: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: WebAuthnKitAPIFuncName CodeUri: lambda-functions/FIDO2KitAPI/ Handler: FIDO2KitAPI.handler @@ -412,16 +460,14 @@ Resources: Environment: Variables: DatabaseName: !Ref DatabaseName - DBAuroraClusterArn: - !Join - - '' - - - - !Sub 'arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:' - - !Ref RDSAuroraServerlessCluster - DBSecretsStoreArn: !Join ['', [!Ref RDSAuroraClusterMasterSecret]] + DBAuroraClusterArn: !Join + - "" + - - !Sub "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:" + - !Ref RDSAuroraServerlessCluster + DBSecretsStoreArn: !Join ["", [!Ref RDSAuroraClusterMasterSecret]] WebAuthnLibFunction: !Ref JavaWebAuthnFuncName - Role: - Fn::GetAtt: + Role: + Fn::GetAtt: - "APILambdaExecutionRole" - "Arn" @@ -429,7 +475,7 @@ Resources: CreateDBSchemaFunction: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: CreateDBSchemaFuncName CodeUri: lambda-functions/CreateDBSchema/ Handler: CreateDBSchema.handler @@ -438,64 +484,69 @@ Resources: Environment: Variables: DatabaseName: !Ref DatabaseName - DBAuroraClusterArn: - !Join - - '' - - - - !Sub 'arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:' - - !Ref RDSAuroraServerlessCluster - DBSecretsStoreArn: !Join ['', [!Ref RDSAuroraClusterMasterSecret]] - Role: - Fn::GetAtt: + DBAuroraClusterArn: !Join + - "" + - - !Sub "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:" + - !Ref RDSAuroraServerlessCluster + DBSecretsStoreArn: !Join ["", [!Ref RDSAuroraClusterMasterSecret]] + Role: + Fn::GetAtt: - "CreateDBSchemaLambdaExecutionRole" - "Arn" - + # IAM Role and Policy for the Lambda function CreateDBSchemaCaller which invokes the createDBSchema function - CreateDBSchemaCallerLambdaExecutionRole: + CreateDBSchemaCallerLambdaExecutionRole: Type: "AWS::IAM::Role" DependsOn: - - CreateDBSchemaFunction + - CreateDBSchemaFunction Properties: AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole Path: "/" Policies: - - - PolicyName: LambdaToCreateDBSchemaPolicy + - PolicyName: LambdaToCreateDBSchemaPolicy PolicyDocument: Version: 2012-10-17 - Statement: - - - Effect: "Allow" + Statement: + - Effect: "Allow" Action: "lambda:InvokeFunction" Resource: - - !Join ['', ['arn:aws:lambda:', !Ref AWS::Region, ':', !Ref AWS::AccountId, ':function:', !Ref CreateDBSchemaFuncName]] - + - !Join [ + "", + [ + "arn:aws:lambda:", + !Ref AWS::Region, + ":", + !Ref AWS::AccountId, + ":function:", + !Ref CreateDBSchemaFuncName, + ], + ] + # AWS Amplify Hosting - Service Role - AmplifyHostingServiceRole: + AmplifyHostingServiceRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Effect: Allow - Principal: - Service: - - amplify.amazonaws.com - Action: - - sts:AssumeRole - Path: "/" - Policies: - - - PolicyName: "WebAuthnKitAmplifyHostingPolicy" - PolicyDocument: + - Effect: Allow + Principal: + Service: + - amplify.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + Policies: + - PolicyName: "WebAuthnKitAmplifyHostingPolicy" + PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" @@ -506,18 +557,18 @@ Resources: AWSAmplifyHostingApp: Condition: isManual Type: AWS::Amplify::App - Properties: - Name: + Properties: + Name: Ref: AmplifyHostingAppName Description: "WebAuthn Starter Kit React Web Client Amplify App" - IAMServiceRole: - Fn::GetAtt: + IAMServiceRole: + Fn::GetAtt: - "AmplifyHostingServiceRole" - "Arn" - CustomRules: + CustomRules: - Source: Target: /index.html - Status: '200' + Status: "200" # AWS Amplify Hosting - Branch AWSAmplifyHostingBranch: @@ -535,11 +586,11 @@ Resources: JavaWebAuthnFunction: Type: AWS::Serverless::Function DependsOn: - - AWSAmplifyHostingBranch + - AWSAmplifyHostingBranch Metadata: BuildMethod: makefile Properties: - FunctionName: + FunctionName: Ref: JavaWebAuthnFuncName CodeUri: lambda-functions/JavaWebAuthnLib/ Runtime: java8.al2 @@ -549,34 +600,29 @@ Resources: # Environment variables for connecting to RDS Environment: Variables: + JAVA_TOOL_OPTIONS: "-Dcom.sun.security.enableCRLDP=true" DatabaseName: !Ref DatabaseName - DBAuroraClusterArn: - !Join - - '' - - - - !Sub 'arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:' - - !Ref RDSAuroraServerlessCluster - DBSecretsStoreArn: !Join ['', [!Ref RDSAuroraClusterMasterSecret]] + DBAuroraClusterArn: !Join + - "" + - - !Sub "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:" + - !Ref RDSAuroraServerlessCluster + DBSecretsStoreArn: !Join ["", [!Ref RDSAuroraClusterMasterSecret]] YUBICO_WEBAUTHN_RP_NAME: "WebAuthn Starter Kit" - YUBICO_WEBAUTHN_RP_ID: - !Join - - '' - - - - Fn::GetAtt: + YUBICO_WEBAUTHN_RP_ID: !Join + - "" + - - Fn::GetAtt: - "AWSAmplifyHostingApp" - "DefaultDomain" - YUBICO_WEBAUTHN_ALLOWED_ORIGINS: - !Join - - '' - - - - "https://" - - !Ref AmplifyHostingBranchName - - "." - - Fn::GetAtt: + YUBICO_WEBAUTHN_ALLOWED_ORIGINS: !Join + - "" + - - "https://" + - !Ref AmplifyHostingBranchName + - "." + - Fn::GetAtt: - "AWSAmplifyHostingApp" - "DefaultDomain" - Role: - Fn::GetAtt: + Role: + Fn::GetAtt: - "JavaLibLambdaExecutionRole" - "Arn" @@ -585,7 +631,7 @@ Resources: CreateDBSchemaFunctionCaller: Type: AWS::Serverless::Function Properties: - FunctionName: + FunctionName: Ref: CreateDBSchemaCallerFuncName InlineCode: | var aws = require('aws-sdk'); @@ -615,11 +661,11 @@ Resources: Description: Invokes the database schema creation function. MemorySize: 128 Timeout: 20 - Role: - Fn::GetAtt: + Role: + Fn::GetAtt: - "CreateDBSchemaCallerLambdaExecutionRole" - "Arn" - + # Amazon Cognito User Pool as our identity provider UserPool: Type: "AWS::Cognito::UserPool" @@ -646,77 +692,77 @@ Resources: # API Gateway - WebAuthn Kit REST API # Using OpenAPI + AWS Extensions (this is a modified OpenAPI 3 export) # - Api: + Api: Type: AWS::ApiGateway::RestApi Properties: - Name: + Name: Ref: WebAuthnKitAPIName - Body: + Body: openapi: "3.0.1" info: version: "2020-09-11T04:20:00Z" title: "WebAuthn Kit API" paths: /users: - options: - responses: - "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" - x-amazon-apigateway-integration: - responses: - default: - statusCode: "200" - responseParameters: - method.response.header.Access-Control-Allow-Origin: !Ref APICorsOrigin - method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders - method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods - responseTemplates: - application/json: '' - passthroughBehavior: "WHEN_NO_TEMPLATES" - requestTemplates: - application/json: "{\"statusCode\": 200}" - type: "mock" - x-amazon-apigateway-any-method: + options: + responses: + "200": + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" + x-amazon-apigateway-integration: responses: - "200": - description: "200 response" - security: + default: + statusCode: "200" + responseParameters: + method.response.header.Access-Control-Allow-Origin: !Ref APICorsOrigin + method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders + method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods + responseTemplates: + application/json: "" + passthroughBehavior: "WHEN_NO_TEMPLATES" + requestTemplates: + application/json: '{"statusCode": 200}' + type: "mock" + x-amazon-apigateway-any-method: + responses: + "200": + description: "200 response" + security: - cognito-userpool-authorizer: [] - x-amazon-apigateway-integration: - type: "aws_proxy" - uri: !Sub - - "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations" - - lambdaArn: !GetAtt "WebAuthnKitAPIFunction.Arn" - responses: - default: - statusCode: "200" - passthroughBehavior: "when_no_match" - httpMethod: "POST" + x-amazon-apigateway-integration: + type: "aws_proxy" + uri: !Sub + - "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations" + - lambdaArn: !GetAtt "WebAuthnKitAPIFunction.Arn" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_match" + httpMethod: "POST" /users/credentials: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -726,17 +772,17 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: "200": description: "200 response" security: - - cognito-userpool-authorizer: [] + - cognito-userpool-authorizer: [] x-amazon-apigateway-integration: type: "aws_proxy" uri: !Sub @@ -749,19 +795,19 @@ Resources: httpMethod: "POST" /users/credentials/pin: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -771,17 +817,17 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: "200": description: "200 response" security: - - cognito-userpool-authorizer: [] + - cognito-userpool-authorizer: [] x-amazon-apigateway-integration: type: "aws_proxy" uri: !Sub @@ -794,19 +840,19 @@ Resources: httpMethod: "POST" /users/credentials/codes: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -816,17 +862,17 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: "200": description: "200 response" security: - - cognito-userpool-authorizer: [] + - cognito-userpool-authorizer: [] x-amazon-apigateway-integration: type: "aws_proxy" uri: !Sub @@ -839,19 +885,19 @@ Resources: httpMethod: "POST" /users/credentials/fido2: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -861,17 +907,17 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: "200": description: "200 response" security: - - cognito-userpool-authorizer: [] + - cognito-userpool-authorizer: [] x-amazon-apigateway-integration: type: "aws_proxy" uri: !Sub @@ -884,19 +930,19 @@ Resources: httpMethod: "POST" /users/credentials/fido2/authenticate: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -906,10 +952,10 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: @@ -929,19 +975,19 @@ Resources: httpMethod: "POST" /users/credentials/fido2/register: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -951,17 +997,17 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: "200": description: "200 response" security: - - cognito-userpool-authorizer: [] + - cognito-userpool-authorizer: [] x-amazon-apigateway-integration: type: "aws_proxy" uri: !Sub @@ -974,19 +1020,19 @@ Resources: httpMethod: "POST" /users/credentials/fido2/register/finish: options: - responses: + responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" x-amazon-apigateway-integration: responses: default: @@ -996,17 +1042,17 @@ Resources: method.response.header.Access-Control-Allow-Headers: !Ref APICorsHeaders method.response.header.Access-Control-Allow-Methods: !Ref APICorsMethods responseTemplates: - application/json: '' + application/json: "" passthroughBehavior: "WHEN_NO_TEMPLATES" requestTemplates: - application/json: "{\"statusCode\": 200}" + application/json: '{"statusCode": 200}' type: "mock" x-amazon-apigateway-any-method: responses: "200": description: "200 response" security: - - cognito-userpool-authorizer: [] + - cognito-userpool-authorizer: [] x-amazon-apigateway-integration: type: "aws_proxy" uri: !Sub @@ -1026,25 +1072,24 @@ Resources: x-amazon-apigateway-authtype: "cognito_user_pools" x-amazon-apigateway-authorizer: type: "cognito_user_pools" - providerARNs: - - !GetAtt UserPool.Arn - Description: 'WebAuthn Starter API for User Credential Management' + providerARNs: + - !GetAtt UserPool.Arn + Description: "WebAuthn Starter API for User Credential Management" FailOnWarnings: true # Create DBSchema Callout DBCreationSchemaCaller: Type: Custom::LambdaDatabaseSchemaCallout Properties: - ServiceToken: - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${CreateDBSchemaCallerFuncName} + ServiceToken: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${CreateDBSchemaCallerFuncName} FunctionName: !Ref CreateDBSchemaFuncName DependsOn: - - CreateDBSchemaFunctionCaller - - CreateDBSchemaFunction + - CreateDBSchemaFunctionCaller + - CreateDBSchemaFunction # Allow API Gateway to invoke Lambda function API from Console APIGWLambdaPermission: Type: "AWS::Lambda::Permission" DependsOn: - - WebAuthnKitAPIFunction + - WebAuthnKitAPIFunction Properties: Action: lambda:InvokeFunction FunctionName: !Ref WebAuthnKitAPIFunction @@ -1097,13 +1142,13 @@ Resources: - ALLOW_USER_SRP_AUTH - ALLOW_REFRESH_TOKEN_AUTH -####### ## ## ######## ######## ## ## ######## -## ## ## ## ## ## ## ## ## ## -## ## ## ## ## ## ## ## ## ## -## ## ## ## ## ######## ## ## ## -## ## ## ## ## ## ## ## ## -## ## ## ## ## ## ## ## ## - ####### ####### ## ## ####### ## +####### ## ## ######## ######## ## ## ######## +## ## ## ## ## ## ## ## ## ## +## ## ## ## ## ## ## ## ## ## +## ## ## ## ## ######## ## ## ## +## ## ## ## ## ## ## ## ## +## ## ## ## ## ## ## ## ## +####### ####### ## ## ####### ## Outputs: UserPoolId: @@ -1119,8 +1164,8 @@ Outputs: Value: !Ref RDSAuroraClusterMasterSecret Description: AWS Secrets Manager Arn DBClusterArn: - Value: !Join - - '' + Value: !Join + - "" - - "arn:aws:rds:" - !Ref AWS::Region - ":" @@ -1131,68 +1176,68 @@ Outputs: - !Ref RDSAuroraServerlessCluster Description: Amazon RDS Cluster Arn APIEndpoint: - Value: - Fn::Sub: 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/dev' + Value: + Fn::Sub: "https://${Api}.execute-api.${AWS::Region}.amazonaws.com/dev" Description: WebAuthn Starter Kit API Gateway Regional Endpoint - AWSConfiguration: - Value: !Join - - '' - - - "{\"CognitoUserPool\": {\"Default\": {\"PoolId\"" + AWSConfiguration: + Value: !Join + - "" + - - '{"CognitoUserPool": {"Default": {"PoolId"' - ":" - - "\"" + - '"' - !Ref UserPool - - "\"" - - ",\"AppClientId\":" - - "\"" + - '"' + - ',"AppClientId":' + - '"' - !Ref UserPoolClient - - "\"" - - ",\"Region\":" - - "\"" + - '"' + - ',"Region":' + - '"' - !Ref AWS::Region - - "\"" - - "}},\"Auth\": {\"Default\": {\"authenticationFlowType\": \"CUSTOM_AUTH\"}}}" + - '"' + - '}},"Auth": {"Default": {"authenticationFlowType": "CUSTOM_AUTH"}}}' Description: AWS constants used for Mobile iOS and Android clients AWSExports: - Value: !Join - - '' - - - "const awsmobile = {\"aws_project_region\"" + Value: !Join + - "" + - - 'const awsmobile = {"aws_project_region"' - ":" - - "\"" + - '"' - !Ref AWS::Region - - "\"" - - ", \"Auth\":" - - " {\"region\":" - - "\"" + - '"' + - ', "Auth":' + - ' {"region":' + - '"' - !Ref AWS::Region - - "\"" - - ", \"userPoolId\":" - - "\"" + - '"' + - ', "userPoolId":' + - '"' - !Ref UserPool - - "\"" - - ", \"userPoolWebClientId\":" - - "\"" + - '"' + - ', "userPoolWebClientId":' + - '"' - !Ref UserPoolClient - - "\"" - - ", \"authenticationFlowType\": \"CUSTOM_AUTH\"}" - - ", \"apiEndpoint\":" - - "\"" - - Fn::Sub: 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/dev' - - "\"" - - ", \"authenticationFlowType\": \"CUSTOM_AUTH\"};" - - '' - - ' export default awsmobile;' + - '"' + - ', "authenticationFlowType": "CUSTOM_AUTH"}' + - ', "apiEndpoint":' + - '"' + - Fn::Sub: "https://${Api}.execute-api.${AWS::Region}.amazonaws.com/dev" + - '"' + - ', "authenticationFlowType": "CUSTOM_AUTH"};' + - "" + - " export default awsmobile;" Description: aws-exports.js configuration for deploying React client to AWS Amplify hosting # AWS Amplify Hosting - Endpoint AmplifyHostingEndpoint: Description: AWS Amplify hosting endpoint for React Web Client - Value: !Join - - '' - - - 'https://' + Value: !Join + - "" + - - "https://" - !Ref AmplifyHostingBranchName - - '.' - - Fn::GetAtt: - - "AWSAmplifyHostingApp" - - "DefaultDomain" + - "." + - Fn::GetAtt: + - "AWSAmplifyHostingApp" + - "DefaultDomain" # AWS Amplify Hosting - App Id AmplifyHostingAppId: Description: AWS Amplify hosting app id diff --git a/clients/web/react/public/i18n/en-US.json b/clients/web/react/public/i18n/en-US.json index 83775c5..9364159 100644 --- a/clients/web/react/public/i18n/en-US.json +++ b/clients/web/react/public/i18n/en-US.json @@ -88,10 +88,12 @@ "last-time-used": "Last used time:", "last-update-time": "Last updates time:", "registration-time": "Registration time:", - "yubico-att-label": "Yubico Device Information:", + "yubico-att-label": "Device Information:", "att-device-name": "Device name:", "att-device-info": "Device info", "att-device-interfaces": "Available interfaces:", + "att-aaguid": "Device AAGUID:", + "att-aaid": "Device AAID:", "edit-cancel-button": "Cancel", "edit-delete-button": "Delete", "edit-save-button": "Save changes" @@ -184,7 +186,7 @@ "If prompted, select Use Face ID", "Allow your device to scan your face" ], - "HELLO": [ + "WINDOWS_HELLO": [ "In the prompt, select Built in Authenticator", "Scan your Fingerprint or enter your PIN" ] diff --git a/clients/web/react/src/RegisterPage/RegisterKeySuccessStep.tsx b/clients/web/react/src/RegisterPage/RegisterKeySuccessStep.tsx index dfe4306..934c6f8 100644 --- a/clients/web/react/src/RegisterPage/RegisterKeySuccessStep.tsx +++ b/clients/web/react/src/RegisterPage/RegisterKeySuccessStep.tsx @@ -64,7 +64,7 @@ const RegisterKeySuccessStep = function ({ setForm, formData, navigation }) { const credentialToUpdate = { credential: { credentialId: { - base64: ls_credential.id, + base64url: ls_credential.id, }, }, credentialNickname: { diff --git a/clients/web/react/src/_components/Credential/AddCredential.tsx b/clients/web/react/src/_components/Credential/AddCredential.tsx index af46a0b..e1edcab 100644 --- a/clients/web/react/src/_components/Credential/AddCredential.tsx +++ b/clients/web/react/src/_components/Credential/AddCredential.tsx @@ -11,6 +11,7 @@ import AddCredentialGuidance from "./AddCredentialGuidance"; // eslint-disable-next-line camelcase import aws_exports from "../../aws-exports"; import { WebAuthnClient } from ".."; +import DetectBrowser from "../../_helpers/DetectBrowser"; // eslint-disable-next-line camelcase axios.defaults.baseURL = aws_exports.apiEndpoint; @@ -133,6 +134,16 @@ const AddCredential = function () { return pinResult.value; } + /** + * Android will not allow for a ResidentKey to be created - and will return an error during the WebAuthn ceremony if RequireResidentKey is True and ResidentKey is set to required + * This method will hide the checkbox to create a resident key from the UI on android device + * @returns false to hide if on android, true if otherwise + */ + function handleAndroidResidentKey() { + if (DetectBrowser.getPlatform().id === "ANDROID_BIOMETRICS") return false; + return true; + } + /** * Primary logic of this method * Calls to the register API, and creates the credential on the security key @@ -211,19 +222,21 @@ const AddCredential = function () { {invalidNickname} ) : null}
- + {handleAndroidResidentKey() && ( + + )} + diff --git a/clients/web/react/src/_components/Credential/AddCredential.tsx b/clients/web/react/src/_components/Credential/AddCredential.tsx index e1edcab..9494fa4 100644 --- a/clients/web/react/src/_components/Credential/AddCredential.tsx +++ b/clients/web/react/src/_components/Credential/AddCredential.tsx @@ -70,26 +70,19 @@ const AddCredential = function () { */ const handleSaveAdd = async () => { setSubmitted(true); - - const result = validate({ keyName: nickname }, constraints); - if (result) { - setInvalidNickname(result.keyName.join(". ")); - } else { - setInvalidNickname(undefined); - setLoading(true); - try { - await register(); - } catch (error) { - console.error( - t("console.error", { - COMPONENT: "AddCredential", - METHOD: "handleSaveAdd()", - REASON: t("console.reason.addCredential0"), - }), - error - ); - setLoading(false); - } + setLoading(true); + try { + await register(); + } catch (error) { + console.error( + t("console.error", { + COMPONENT: "AddCredential", + METHOD: "handleSaveAdd()", + REASON: t("console.reason.addCredential0"), + }), + error + ); + setLoading(false); } }; @@ -158,7 +151,6 @@ const AddCredential = function () { * More information can be found here: https://www.w3.org/TR/webauthn-2/#enum-attachment */ await WebAuthnClient.registerNewCredential( - nickname, isResidentKey, "CROSS_PLATFORM", registerUV @@ -200,28 +192,6 @@ const AddCredential = function () { )} - - { - if (ev.key === "Enter") { - handleSaveAdd(); - ev.preventDefault(); - } - }} - /> - {invalidNickname ? ( - {invalidNickname} - ) : null} -
{handleAndroidResidentKey() && (