diff --git a/.travis.yml b/.travis.yml index 6524ff2e3..6b6f7219c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: java +branches: + except: + - tmp + env: global: secure: sX5sJd2EUgzIT7uQN0YxA3faVHymBG/QPZ/St5IPqoQIXjZAMYBM0D1MrVOYaSOhgVKOJt+5vwCYU7MlY9Ha0rUPJgUPT+6CkVgUVCsQ1e8srAzaYp4ceIYaW2XpUIwhKHPBezulV3nLANRs0FibEN+eqTgL5A/qKtsU49BtQ1iUAVFFOzGcR48avo1UYxS0FLw+7MRLgH5NA6KJVHiGChx9P3oLYAhPylgDzRv6iFf5H5v9azQI4eLo6bSQwm++j0UpH4t8m+at7eGuzNsadYY0M9SoUwuJxQZiwtImYJJtGJD92QtV9m+yny4+RocXchgZDj3e9vx06ZqXaeF3U3o49YUX5ACerVV12yOxGZsuuxfevaQa9Mk4xEOwGkhva5I+8vfo8MRxm7ymelExn25zpsMlmj6GjBio3z1q/FGYdyXrcGoVNrvAgozs+0yW2jYtDVo7DNu8J2mur/C/gmi+xA6rkuEJQIQ3hWuWYVe7DUzdii5MG9/9AdwI14b3uyezh1EJ8tza5MScDQijTvD9sGxarruKS59VuJapqrJSU5E87CnlU6gQx7qXJVGvpTXZOw7ZzsdszSDQ3Jc9uNBSdtBQ2i7egEyTE+RQWsdtje/H0s3ZYyIw8qrQ1kIUDQKk7jl8Uvwf+zn/36JBgZMVIIO0hmDFnyB9wBGd7lk= diff --git a/NEWS b/NEWS index 10839bed7..369fd40f7 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,7 @@ == Version 1.0.0 == * Fixed URL in artifact POM +* Improved a few javadoc wordings == Version 0.8.0 == diff --git a/README b/README index 679b6f85b..29026462f 100644 --- a/README +++ b/README @@ -1,8 +1,8 @@ -== java-webauthn-server - -_Note: This is a work in progress. The https://www.w3.org/TR/webauthn/[Web -Authentication standard] is not yet finished, and additional pre-1.0 releases of -this library may introduce breaking API changes._ +java-webauthn-server +==================== +:toc: +:toc-placement: macro +:toc-title: image:https://travis-ci.org/Yubico/java-webauthn-server.svg?branch=master["Build Status", link="https://travis-ci.org/Yubico/java-webauthn-server"] image:https://coveralls.io/repos/github/Yubico/java-webauthn-server/badge.svg["Coverage Status", link="https://coveralls.io/github/Yubico/java-webauthn-server"] @@ -14,18 +14,293 @@ for a server to support Web Authentication. This includes registering authenticators and authenticating registered authenticators. -=== Planned breaking changes +== Table of contents + +toc::[] + + +== Dependency configuration + +Maven: + +---------- + + com.yubico + webauthn-server-core + 1.0.0-RC2 + compile + +---------- + +Gradle: + +---------- +compile 'com.yubico:webauthn-server-core:1.0.0-RC2' +---------- + + +== Features + +- Generates request objects suitable as parameters to + `navigator.credentials.create()` and `.get()` +- Performs all necessary + https://www.w3.org/TR/webauthn/#rp-operations[validation logic] on the + response from the client +- Optionally integrates with a "metadata service" to verify + https://www.w3.org/TR/webauthn/#sctn-attestation[authenticator attestations] + and annotate responses with additional authenticator metadata + + +=== Non-features + +This library has no concept of accounts, sessions, permissions or identity +federation, and it's not an authentication framework; it only deals with +executing the WebAuthn authentication mechanism. Sessions, account management +and other higher level concepts can make use of this authentication mechanism, +but the authentication mechanism alone does not make a security system. + + +== Documentation + +See the +link:https://yubico.github.io/java-webauthn-server/webauthn-server-core/com/yubico/webauthn/package-summary.html[Javadoc] +for in-depth API documentation. + + +== Quick start + +Implement the +link:https://yubico.github.io/java-webauthn-server/webauthn-server-core/com/yubico/webauthn/CredentialRepository.html[`CredentialRepository`] +interface with your database access logic. See +link:https://github.com/Yubico/java-webauthn-server/blob/master/webauthn-server-demo/src/main/java/demo/webauthn/InMemoryRegistrationStorage.java[`InMemoryRegistrationStorage`] +for an example. + +Instantiate the +link:https://yubico.github.io/java-webauthn-server/webauthn-server-core/com/yubico/webauthn/RelyingParty.html[`RelyingParty`] +class: + +[source,java] +---------- +RelyingPartyIdentity rpIdentity = RelyingPartyIdentity.builder() + .id("example.com") + .name("Example Application") + .build(); + +RelyingParty rp = RelyingParty.builder() + .identity(rpIdentity) + .credentialRepository(new MyCredentialRepository()) + .build(); +---------- + + +=== Registration + +Initiate a registration ceremony: + +[source,java] +---------- +byte[] userHandle = new byte[64]; +random.nextBytes(userHandle); + +PublicKeyCredentialCreationOptions request = rp.startRegistration(StartRegistrationOptions.builder() + .user(UserIdentity.builder() + .name("alice") + .displayName("Alice Hypothetical") + .id(new ByteArray(userHandle)) + .build()) + .build()); +---------- + +Serialize `request` to JSON and send it to the client: + +[source,java] +---------- +import com.fasterxml.jackson.databind.ObjectMapper; + +ObjectMapper jsonMapper = new ObjectMapper() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .setSerializationInclusion(Include.NON_ABSENT) + .registerModule(new Jdk8Module()); + +String json = jsonMapper.writeValueAsString(request); +return json; +---------- + +Get the response from the client: + +[source,java] +---------- +String responseJson = /* ... */; +PublicKeyCredential pkc = + jsonMapper.readValue(responseJson, new TypeReference>(){}); +---------- + +Validate the response: + +[source,java] +---------- +try { + RegistrationResult result = rp.finishRegistration(FinishRegistrationOptions.builder() + .request(request) + .response(pkc) + .build()); +} catch (RegistrationFailedException e) { /* ... */ } +---------- + +Update your database: + +[source,java] +---------- +storeCredential("alice", result.getKeyId(), result.getPublicKeyCose()); +---------- + + +=== Authentication + +Initiate an authentication ceremony: + + +[source,java] +---------- +AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder() + .username(Optional.of("alice")) + .build()); +String json = jsonMapper.writeValueAsString(request); +return json; +---------- + +Validate the response: + +[source,java] +---------- +String responseJson = /* ... */; + +PublicKeyCredential pkc = + jsonMapper.readValue(responseJson, new TypeReference>() { +}); + +try { + AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder() + .request(request) + .response(pkc) + .build()); + + if (result.isSuccess()) { + return result.getUsername(); + } +} catch (AssertionFailedException e) { /* ... */ } +throw new RuntimeException("Authentication failed"); +---------- + +For more detailed example usage, see +link:webauthn-server-demo[`webauthn-server-demo`] for a complete demo server. + + +== Architecture + +The library tries to place as few requirements on the overall application +architecture as possible. For this reason it is stateless and free from side +effects, and does not directly interact with any database. This means it is +database agnostic and thread safe. The following diagram illustrates an example +architecture for an application using the library. + +image::https://raw.githubusercontent.com/Yubico/java-webauthn-server/master/docs/img/demo-architecture.svg["Example application architecture",align="center"] + +The application manages all state and database access, and communicates with the +library via POJO representations of requests and responses. The following +diagram illustrates the data flow during a WebAuthn registration or +authentication ceremony. + +image::https://raw.githubusercontent.com/Yubico/java-webauthn-server/master/docs/img/demo-sequence-diagram.svg["WebAuthn ceremony sequence diagram",align="center"] + +In this diagram, the *Client* is the user's browser and the application's +client-side scripts. The *Server* is the application and its business logic, the +*Library* is this library, and the *Users* database stores registered WebAuthn +credentials. + +. The client requests to start the ceremony, for example by submitting a form. + The `username` may or may not be known at this point. For example, the user + might be requesting to create a new account, or we might be using + username-less authentication. + +. If the user does not already have a + https://www.w3.org/TR/webauthn/#user-handle[user handle], the application + creates one in some application-specific way. + +. The application may choose to authenticate the user with a password or the + like before proceeding. + +. The application calls one of the library's "start" methods to generate a + parameter object to be passed to `navigator.credentials.create()` or `.get()` + on the client. + +. The library generates a random challenge and an assortment of other arguments + depending on configuration set by the application. + +. If the `username` is known, the library uses a read-only database adapter + provided by the application to look up the user's credentials. + +. The returned list of https://www.w3.org/TR/webauthn/#credential-id[credential + IDs] is used to populate the + https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-excludecredentials[`excludeCredentials`] + or + https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-allowcredentials[`allowCredentials`] + parameter. + +. The library returns a `request` object which can be serialized to JSON and + passed as the `publicKey` argument to `navigator.credentials.create()` or + `.get()`. For registration ceremonies this will be a + https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions[`PublicKeyCredentialCreationOptions`], + and for authentication ceremonies it will be a + https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions[`PublicKeyCredentialRequestOptions`]. + The application stores the `request` in temporary storage. + +. The application's client-side script runs `navigator.credentials.create()` or + `.get()` with `response` as the `publicKey` argument. + +. The user confirms the operation and the client returns a + https://www.w3.org/TR/webauthn/#public-key-credential[`PublicKeyCredential`] + object `response` to the application. + +. The application retrieves the `request` from temporary storage and passes + `request` and `response` to one of the library's "finish" methods to run the + response validation logic. + +. The library verifies that the `response` contents - challenge, origin, etc. - + are valid. + +. If this is an authentication ceremony, the library uses the database adapter + to look up the public key for the credential named in `response.id`. + +. The database adapter returns the public key. + +. The library verifies the authentication signature. + +. The library returns a POJO representation of the result of the ceremony. For + registration ceremonies, this will include the credential ID and public key of + the new credential. The application may also opt in to also getting + information about the authenticator model and whether the authenticator + attestation is trusted. For authentication ceremonies, this will include the + username and user handle, the credential ID of the credential used, and the + new https://www.w3.org/TR/webauthn/#signature-counter[signature counter] value + for the credential. + +. The application inspects the result object and takes any appropriate actions + as defined by its business logic. -None. +. If the result is not satisfactory, the application reports failure to the + client. +. If the result is satisfactory, the application proceeds with storing the new + credential if this is a registration ceremony. -=== Example Usage +. If this is an authentication ceremony, the application updates the signature + counter stored in the database for the credential. -See link:webauthn-server-demo[`webauthn-server-demo`] for a complete demo -server, which stores authenticator registrations temporarily in memory. +. Finally, the application reports success and resumes its business logic. -=== Building +== Building Use the included https://docs.gradle.org/current/userguide/gradle_wrapper.html[Gradle wrapper] to diff --git a/docs/img/demo-architecture.svg b/docs/img/demo-architecture.svg new file mode 100644 index 000000000..72e092654 --- /dev/null +++ b/docs/img/demo-architecture.svg @@ -0,0 +1,340 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/demo-sequence-diagram.plantuml b/docs/img/demo-sequence-diagram.plantuml new file mode 100644 index 000000000..7d632df75 --- /dev/null +++ b/docs/img/demo-sequence-diagram.plantuml @@ -0,0 +1,31 @@ +@startuml +'Render using PlantUML: http://plantuml.com/ + +participant Client +participant Server +participant Library +database Users +autonumber + +Client -> Server: ""start(username?)"" +Server -> Server: Create userHandle? +Server -> Server: Verify password? +Server -> Library: ""start(username?, userHandle?)"" +Library -> Library: Generate challenge +Library -> Users: ""listCredentials(username)"" +Users --> Library: Credential IDs +Library --> Server: ""request: PublicKeyCredential*Options"" +Server -> Client: ""navigator.credentials.*(request)"" +Client --> Server: ""response: PublicKeyCredential"" +Server -> Library: ""finish(request, response)"" +Library -> Library: Validate ""(request, response)"" +Library -> Users: ""getPublicKey(response.id)"" +Users --> Library: Public key +Library -> Library: Verify ""response"" signature +Library --> Server: ""result: Result"" +Server -> Server: Inspect ""result"" +Server ->x Client: Report failure? +Server -> Users: Store new credential? +Server -> Users: Update signature count? +Server ->o Client: Report success? +@enduml \ No newline at end of file diff --git a/docs/img/demo-sequence-diagram.svg b/docs/img/demo-sequence-diagram.svg new file mode 100644 index 000000000..fef27db34 --- /dev/null +++ b/docs/img/demo-sequence-diagram.svg @@ -0,0 +1,43 @@ + +ClientClientServerServerLibraryLibraryUsersUsers1start(username?)2Create userHandle?3Verify password?4start(username?, userHandle?)5Generate challenge6listCredentials(username)7Credential IDs8request: PublicKeyCredential*Options9navigator.credentials.*(request)10response: PublicKeyCredential11finish(request, response)12Validate(request, response)13getPublicKey(response.id)14Public key15Verifyresponsesignature16result: Result17Inspectresult18Report failure?19Store new credential?20Update signature count?21Report success? \ No newline at end of file diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java index 5bfbdd251..129eb699e 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/StandardMetadataService.java @@ -105,8 +105,8 @@ public Attestation getCachedAttestation(String attestationCertificateFingerprint * *

* If the certificate chain is not trusted, the method returns an untrusted - * attestation populated with transports information found embedded in the - * attestation certificate. + * attestation populated with {@link Attestation#getTransports() transports} + * information found embedded in the attestation certificate. *

* *

diff --git a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java index 5a6b62c0d..105fce61a 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/webauthn/attestation/TrustResolver.java @@ -46,9 +46,9 @@ default Optional resolveTrustAnchor(X509Certificate attestation * @param attestationCertificate The attestation certificate * @param caCertificateChain Zero or more certificates, of which the first * has signed attestationCertificate and each of the - * rest has signed the previous in order - * @return A trusted root certificate from which there exists a signature - * path to attestationCertificate, if one exists. + * remaining certificates has signed the certificate preceding it. + * @return A trusted root certificate from which there is a signature path + * to attestationCertificate, if one exists. */ Optional resolveTrustAnchor(X509Certificate attestationCertificate, List caCertificateChain); diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index d6404f796..27d559b2e 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -779,7 +779,10 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv describe("client extension outputs in clientExtensionResults are as expected, considering the client extension input values that were given as the extensions option in the get() call. In particular, any extension identifier values in the clientExtensionResults MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of \"are as expected\" is specific to the Relying Party and which extensions are in use.") { it("Fails if clientExtensionResults is not a subset of the extensions requested by the Relying Party.") { - forAll(unrequestedAssertionExtensions, minSuccessful(1)) { case (extensionInputs, clientExtensionOutputs) => + val extensionInputs = AssertionExtensionInputs.builder().build() + val clientExtensionOutputs = ClientAssertionExtensionOutputs.builder().appid(Optional.of(true)).build() + + // forAll(unrequestedAssertionExtensions, minSuccessful(1)) { case (extensionInputs, clientExtensionOutputs) => val steps = finishAssertion( requestedExtensions = extensionInputs, clientExtensionResults = clientExtensionOutputs @@ -789,7 +792,7 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv step.validations shouldBe a [Failure[_]] step.validations.failed.get shouldBe an [IllegalArgumentException] step.tryNext shouldBe a [Failure[_]] - } + // } } it("Succeeds if clientExtensionResults is a subset of the extensions requested by the Relying Party.") {