Search in sources :

Example 1 with AttestationException

use of io.vertx.ext.auth.webauthn.impl.attestation.AttestationException in project quarkus by quarkusio.

the class WebAuthnController method callback.

/**
 * Endpoint for callback
 *
 * @param ctx the current request
 */
public void callback(RoutingContext ctx) {
    try {
        // might throw runtime exception if there's no json or is bad formed
        final JsonObject webauthnResp = ctx.getBodyAsJson();
        // input validation
        if (webauthnResp == null || !containsRequiredString(webauthnResp, "id") || !containsRequiredString(webauthnResp, "rawId") || !containsRequiredObject(webauthnResp, "response") || !containsOptionalString(webauthnResp.getJsonObject("response"), "userHandle") || !containsRequiredString(webauthnResp, "type") || !"public-key".equals(webauthnResp.getString("type"))) {
            ctx.fail(400, new IllegalArgumentException("Response missing one or more of id/rawId/response[.userHandle]/type fields, or type is not public-key"));
            return;
        }
        RestoreResult challenge = authMech.getLoginManager().restore(ctx, CHALLENGE_COOKIE);
        RestoreResult username = authMech.getLoginManager().restore(ctx, USERNAME_COOKIE);
        if (challenge == null || challenge.getPrincipal() == null || challenge.getPrincipal().isEmpty() || username == null || username.getPrincipal() == null || username.getPrincipal().isEmpty()) {
            ctx.fail(400, new IllegalArgumentException("Missing challenge or username"));
            return;
        }
        ManagedContext requestContext = Arc.container().requestContext();
        requestContext.activate();
        ContextState contextState = requestContext.getState();
        // input basic validation is OK
        // authInfo
        WebAuthnCredentials credentials = new WebAuthnCredentials().setOrigin(origin).setDomain(domain).setChallenge(challenge.getPrincipal()).setUsername(username.getPrincipal()).setWebauthn(webauthnResp);
        identityProviderManager.authenticate(HttpSecurityUtils.setRoutingContextAttribute(new WebAuthnAuthenticationRequest(credentials), ctx)).subscribe().with(new Consumer<SecurityIdentity>() {

            @Override
            public void accept(SecurityIdentity identity) {
                requestContext.destroy(contextState);
                // invalidate the challenge
                WebAuthnSecurity.removeCookie(ctx, WebAuthnController.CHALLENGE_COOKIE);
                WebAuthnSecurity.removeCookie(ctx, WebAuthnController.USERNAME_COOKIE);
                try {
                    authMech.getLoginManager().save(identity, ctx, null, ctx.request().isSSL());
                    ok(ctx);
                } catch (Throwable t) {
                    log.error("Unable to complete post authentication", t);
                    ctx.fail(t);
                }
            }
        }, new Consumer<Throwable>() {

            @Override
            public void accept(Throwable throwable) {
                requestContext.terminate();
                if (throwable instanceof AttestationException) {
                    ctx.fail(400, throwable);
                } else {
                    ctx.fail(throwable);
                }
            }
        });
    } catch (IllegalArgumentException e) {
        ctx.fail(400, e);
    } catch (RuntimeException e) {
        ctx.fail(e);
    }
}
Also used : AttestationException(io.vertx.ext.auth.webauthn.impl.attestation.AttestationException) RestoreResult(io.quarkus.vertx.http.runtime.security.PersistentLoginManager.RestoreResult) JsonObject(io.vertx.core.json.JsonObject) SecurityIdentity(io.quarkus.security.identity.SecurityIdentity) WebAuthnCredentials(io.vertx.ext.auth.webauthn.WebAuthnCredentials) ContextState(io.quarkus.arc.InjectableContext.ContextState) ManagedContext(io.quarkus.arc.ManagedContext)

Example 2 with AttestationException

use of io.vertx.ext.auth.webauthn.impl.attestation.AttestationException in project vertx-web by vert-x3.

the class WebAuthnHandlerImpl method mountResponse.

private void mountResponse() {
    response.method(HttpMethod.POST).order(order - 1).handler(ctx -> {
        try {
            // might throw runtime exception if there's no json or is bad formed
            final JsonObject webauthnResp = ctx.body().asJsonObject();
            // input validation
            if (webauthnResp == null || !containsRequiredString(webauthnResp, "id") || !containsRequiredString(webauthnResp, "rawId") || !containsRequiredObject(webauthnResp, "response") || !containsOptionalString(webauthnResp.getJsonObject("response"), "userHandle") || !containsRequiredString(webauthnResp, "type") || !"public-key".equals(webauthnResp.getString("type"))) {
                ctx.fail(400, new IllegalArgumentException("Response missing one or more of id/rawId/response[.userHandle]/type fields, or type is not public-key"));
                return;
            }
            // input basic validation is OK
            final Session session = ctx.session();
            if (session == null) {
                ctx.fail(500, new IllegalStateException("No session or session handler is missing."));
                return;
            }
            authProvider.authenticate(// authInfo
            new WebAuthnCredentials().setOrigin(origin).setDomain(domain).setChallenge(session.get("challenge")).setUsername(session.get("username")).setWebauthn(webauthnResp), authenticate -> {
                // invalidate the challenge
                session.remove("challenge");
                if (authenticate.succeeded()) {
                    final User user = authenticate.result();
                    // save the user into the context
                    ctx.setUser(user);
                    // the user has upgraded from unauthenticated to authenticated
                    // session should be upgraded as recommended by owasp
                    session.regenerateId();
                    ok(ctx);
                } else {
                    Throwable cause = authenticate.cause();
                    if (cause instanceof AttestationException) {
                        ctx.fail(400, cause);
                    } else {
                        ctx.fail(cause);
                    }
                }
            });
        } catch (IllegalArgumentException e) {
            ctx.fail(400, e);
        } catch (RuntimeException e) {
            ctx.fail(e);
        }
    });
}
Also used : User(io.vertx.ext.auth.User) AttestationException(io.vertx.ext.auth.webauthn.impl.attestation.AttestationException) WebAuthnCredentials(io.vertx.ext.auth.webauthn.WebAuthnCredentials) JsonObject(io.vertx.core.json.JsonObject) Session(io.vertx.ext.web.Session)

Example 3 with AttestationException

use of io.vertx.ext.auth.webauthn.impl.attestation.AttestationException in project vertx-auth by vert-x3.

the class WebAuthnImpl method verifyWebAuthNGet.

/**
 * Verify navigator.credentials.get response
 *
 * @param request        - The request as received by the {@link #authenticate(Credentials, Handler)} method.
 * @param clientDataJSON - The extracted clientDataJSON
 * @param credential     - Credential from Database
 */
private long verifyWebAuthNGet(WebAuthnCredentials request, byte[] clientDataJSON, JsonObject credential) throws IOException, AttestationException, NoSuchAlgorithmException {
    JsonObject response = request.getWebauthn().getJsonObject("response");
    // Step #5
    // parse auth data
    byte[] authenticatorData = base64UrlDecode(response.getString("authenticatorData"));
    AuthData authData = new AuthData(authenticatorData);
    // One extra check, we can verify that the relying party id is for the given domain
    if (request.getDomain() != null) {
        if (!MessageDigest.isEqual(authData.getRpIdHash(), hash("SHA-256", request.getDomain().getBytes(StandardCharsets.UTF_8)))) {
            throw new AttestationException("WebAuthn rpIdHash invalid (the domain does not match the AuthData)");
        }
    }
    // check that the user was either validated or present
    switch(options.getUserVerification()) {
        case REQUIRED:
            if (!authData.is(AuthData.USER_VERIFIED) || !authData.is(AuthData.USER_PRESENT)) {
                throw new AttestationException("User was either not verified or not present during credentials creation");
            }
            break;
        case PREFERRED:
            if (!authData.is(AuthData.USER_VERIFIED) && !authData.is(AuthData.USER_PRESENT)) {
                LOG.warn("User was either not verified or present during credentials creation");
            }
            break;
        case DISCOURAGED:
            if (authData.is(AuthData.USER_VERIFIED) || authData.is(AuthData.USER_PRESENT)) {
                LOG.info("User was either verified or present during credentials creation");
            }
            break;
    }
    // From here we start the validation that is specific for webauthn.get
    // Step webauthn.get#1
    // hash clientDataJSON with SHA-256
    byte[] clientDataHash = hash("SHA-256", clientDataJSON);
    // Step webauthn.get#2
    // concat authenticatorData and clientDataHash
    Buffer signatureBase = Buffer.buffer().appendBytes(authenticatorData).appendBytes(clientDataHash);
    // Using previously saved public key, verify signature over signatureBase.
    try (JsonParser parser = CBOR.cborParser(credential.getString("publicKey"))) {
        // the decoded credential primary as a JWK
        JWK publicKey = CWK.toJWK(new JsonObject(CBOR.<Map<String, Object>>parse(parser)));
        // convert signature to buffer
        byte[] signature = base64UrlDecode(response.getString("signature"));
        // verify signature
        JWS jws = new JWS(publicKey);
        if (!jws.verify(signature, signatureBase.getBytes())) {
            // Step webauthn.get#4
            // If you can’t verify signature multiple times, potentially raise the
            // alarm as phishing attempt most likely is occurring.
            LOG.warn("Failed to verify signature for key: " + credential.getString("publicKey"));
            throw new AttestationException("Failed to verify the signature!");
        }
        // and this step should be skipped
        if (authData.getSignCounter() != 0 || credential.getLong("counter") != 0) {
            // If it’s not, potentially raise the alarm as replay attack may have occurred.
            if (authData.getSignCounter() != 0 && authData.getSignCounter() <= credential.getLong("counter", 0L)) {
                throw new AttestationException("Authenticator counter did not increase!");
            }
        }
        // return the counter so it can be updated on the store
        return authData.getSignCounter();
    }
}
Also used : Buffer(io.vertx.core.buffer.Buffer) AttestationException(io.vertx.ext.auth.webauthn.impl.attestation.AttestationException) JWS(io.vertx.ext.auth.impl.jose.JWS) JsonObject(io.vertx.core.json.JsonObject) JsonParser(com.fasterxml.jackson.core.JsonParser) JWK(io.vertx.ext.auth.impl.jose.JWK)

Example 4 with AttestationException

use of io.vertx.ext.auth.webauthn.impl.attestation.AttestationException in project vertx-auth by vert-x3.

the class WebAuthnImpl method authenticate.

@Override
public void authenticate(Credentials credentials, Handler<AsyncResult<User>> handler) {
    try {
        // cast
        WebAuthnCredentials authInfo = (WebAuthnCredentials) credentials;
        // check
        authInfo.checkValid(null);
        // The basic data supplied with any kind of validation is:
        // {
        // "rawId": "base64url",
        // "id": "base64url",
        // "response": {
        // "clientDataJSON": "base64url"
        // }
        // }
        final JsonObject webauthn = authInfo.getWebauthn();
        // verifying the webauthn response starts here:
        // regardless of the request the first 6 steps are always executed:
        // 1. Decode ClientDataJSON
        // 2. Check that challenge is set to the challenge you’ve sent
        // 3. Check that origin is set to the the origin of your website. If it’s not raise the alarm, and log the event, because someone tried to phish your user
        // 4. Check that type is set to either “webauthn.create” or “webauthn.get”.
        // 5. Parse authData or authenticatorData.
        // 6. Check that flags have UV or UP flags set.
        // STEP #1
        // The client data (or session) is a base64 url encoded JSON
        // we specifically keep track of the binary representation as it will be
        // used later on during validation to verify signatures for tampering
        final byte[] clientDataJSON = base64UrlDecode(webauthn.getJsonObject("response").getString("clientDataJSON"));
        JsonObject clientData = new JsonObject(Buffer.buffer(clientDataJSON));
        // Verify challenge is match with session
        if (!authInfo.getChallenge().equals(clientData.getString("challenge"))) {
            handler.handle(Future.failedFuture("Challenges don't match!"));
            return;
        }
        // If the auth info object contains an Origin we can verify it:
        if (authInfo.getOrigin() != null) {
            if (!authInfo.getOrigin().equals(clientData.getString("origin"))) {
                handler.handle(Future.failedFuture("Origins don't match!"));
                return;
            }
        }
        // optional data
        if (clientData.containsKey("tokenBinding")) {
            JsonObject tokenBinding = clientData.getJsonObject("tokenBinding");
            if (tokenBinding == null) {
                handler.handle(Future.failedFuture("Invalid clientDataJSON.tokenBinding"));
                return;
            }
            // in this case we need to check the status
            switch(tokenBinding.getString("status")) {
                case "present":
                case "supported":
                case "not-supported":
                    // OK
                    break;
                default:
                    handler.handle(Future.failedFuture("Invalid clientDataJSON.tokenBinding.status"));
                    return;
            }
        }
        final String username = authInfo.getUsername();
        // Verify that the type is valid and that is "webauthn.create" or "webauthn.get"
        if (!clientData.containsKey("type")) {
            handler.handle(Future.failedFuture("Missing type on client data"));
            return;
        }
        switch(clientData.getString("type")) {
            case "webauthn.create":
                // we always need a username to register
                if (username == null) {
                    handler.handle(Future.failedFuture("username can't be null!"));
                    return;
                }
                try {
                    final Authenticator authrInfo = verifyWebAuthNCreate(authInfo, clientDataJSON);
                    // by default the store can upsert if a credential is missing, the user has been verified so it is valid
                    // the store however might disallow this operation
                    authrInfo.setUserName(username);
                    // the create challenge is complete we can finally safe this
                    // new authenticator to the storage
                    updater.apply(authrInfo).onFailure(err -> handler.handle(Future.failedFuture(err))).onSuccess(stored -> handler.handle(Future.succeededFuture(User.create(authrInfo.toJson()))));
                } catch (RuntimeException | AttestationException | IOException | NoSuchAlgorithmException e) {
                    handler.handle(Future.failedFuture(e));
                }
                return;
            case "webauthn.get":
                Authenticator query = new Authenticator();
                if (options.getRequireResidentKey()) {
                    // username are not provided (RK) we now need to lookup by id
                    query.setCredID(webauthn.getString("id"));
                } else {
                    // username can't be null
                    if (username == null) {
                        handler.handle(Future.failedFuture("username can't be null!"));
                        return;
                    }
                    query.setUserName(username);
                }
                fetcher.apply(query).onFailure(err -> handler.handle(Future.failedFuture(err))).onSuccess(authenticators -> {
                    if (authenticators == null) {
                        authenticators = Collections.emptyList();
                    }
                    // This means that we **must** lookup the list for the right authenticator
                    for (Authenticator authenticator : authenticators) {
                        if (webauthn.getString("id").equals(authenticator.getCredID())) {
                            try {
                                final long counter = verifyWebAuthNGet(authInfo, clientDataJSON, authenticator.toJson());
                                // update the counter on the authenticator
                                authenticator.setCounter(counter);
                                // update the credential (the important here is to update the counter)
                                updater.apply(authenticator).onFailure(err -> handler.handle(Future.failedFuture(err))).onSuccess(stored -> handler.handle(Future.succeededFuture(User.create(authenticator.toJson()))));
                            } catch (RuntimeException | AttestationException | IOException | NoSuchAlgorithmException e) {
                                handler.handle(Future.failedFuture(e));
                            }
                            return;
                        }
                    }
                    // No valid authenticator was found
                    handler.handle(Future.failedFuture("Cannot find authenticator with id: " + webauthn.getString("id")));
                });
                return;
            default:
                handler.handle(Future.failedFuture("Can not determine type of response!"));
        }
    } catch (RuntimeException e) {
        handler.handle(Future.failedFuture(e));
    }
}
Also used : Codec.base64UrlEncode(io.vertx.ext.auth.impl.Codec.base64UrlEncode) java.util(java.util) JWK(io.vertx.ext.auth.impl.jose.JWK) LoggerFactory(io.vertx.core.impl.logging.LoggerFactory) MessageDigest(java.security.MessageDigest) CWK(io.vertx.ext.auth.impl.cose.CWK) io.vertx.ext.auth.webauthn(io.vertx.ext.auth.webauthn) Attestation.hash(io.vertx.ext.auth.webauthn.impl.attestation.Attestation.hash) Function(java.util.function.Function) MetaDataServiceImpl(io.vertx.ext.auth.webauthn.impl.metadata.MetaDataServiceImpl) JsonObject(io.vertx.core.json.JsonObject) Attestation(io.vertx.ext.auth.webauthn.impl.attestation.Attestation) Codec.base64UrlDecode(io.vertx.ext.auth.impl.Codec.base64UrlDecode) AsyncResult(io.vertx.core.AsyncResult) JWS(io.vertx.ext.auth.impl.jose.JWS) Credentials(io.vertx.ext.auth.authentication.Credentials) Logger(io.vertx.core.impl.logging.Logger) JsonParser(com.fasterxml.jackson.core.JsonParser) Vertx(io.vertx.core.Vertx) IOException(java.io.IOException) Future(io.vertx.core.Future) StandardCharsets(java.nio.charset.StandardCharsets) JsonArray(io.vertx.core.json.JsonArray) User(io.vertx.ext.auth.User) Buffer(io.vertx.core.buffer.Buffer) AttestationException(io.vertx.ext.auth.webauthn.impl.attestation.AttestationException) NoSuchAlgorithmException(java.security.NoSuchAlgorithmException) VertxContextPRNG(io.vertx.ext.auth.VertxContextPRNG) Handler(io.vertx.core.Handler) AttestationException(io.vertx.ext.auth.webauthn.impl.attestation.AttestationException) JsonObject(io.vertx.core.json.JsonObject) IOException(java.io.IOException) NoSuchAlgorithmException(java.security.NoSuchAlgorithmException)

Example 5 with AttestationException

use of io.vertx.ext.auth.webauthn.impl.attestation.AttestationException in project vertx-auth by vert-x3.

the class MetaData method verifyMetadata.

@Nullable
public JsonObject verifyMetadata(String aaguid, PublicKeyCredential alg, List<X509Certificate> x5c, X509Certificate rootCert, boolean includesRoot) throws MetaDataException, AttestationException, NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, CertificateException {
    // If available, validate attestation alg and x5c with info in the metadata statement
    MetaDataEntry entry = store.get(aaguid);
    if (entry != null) {
        entry.checkValid();
        // Make sure the alg in the attestation statement matches the one specified in the metadata
        switch(entry.version()) {
            case 2:
                if (alg != toJOSEAlg(entry.statement().getInteger("authenticationAlgorithm"))) {
                    throw new AttestationException("Attestation alg did not match metadata auth alg");
                }
                break;
            case 3:
                // in MDS3 this field is an array
                boolean found = false;
                for (Object el : entry.statement().getJsonArray("authenticationAlgorithms", EMTPY)) {
                    if (alg == toJOSEAlg((String) el)) {
                        // OK
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    throw new AttestationException("Attestation alg did not match metadata auth alg");
                }
                break;
            default:
                throw new AttestationException("Unsupported metadata version: " + entry.version());
        }
        if (x5c != null) {
            // make a copy before we start
            x5c = new ArrayList<>(x5c);
            // Using MDS or Metadata Statements, for each attestationRoot in attestationRootCertificates:
            // append attestation root to the end of the header.x5c, and try verifying certificate chain.
            // If none succeed, throw an error
            JsonArray attestationRootCertificates = entry.statement().getJsonArray("attestationRootCertificates");
            if (attestationRootCertificates == null || attestationRootCertificates.size() == 0) {
                if (rootCert != null) {
                    x5c.add(rootCert);
                }
                CertificateHelper.checkValidity(x5c, includesRoot, options.getRootCrls());
            } else {
                boolean chainValid = false;
                for (int i = 0; i < attestationRootCertificates.size(); i++) {
                    try {
                        // add the metadata root certificate
                        x5c.add(JWS.parseX5c(attestationRootCertificates.getString(i)));
                        CertificateHelper.checkValidity(x5c, options.getRootCrls());
                        chainValid = true;
                        break;
                    } catch (CertificateException e) {
                        // remove the previously added certificate
                        x5c.remove(x5c.size() - 1);
                    // continue
                    }
                }
                if (!chainValid) {
                    throw new AttestationException("Certificate Chain not valid for metadata");
                }
            }
        }
        return entry.statement();
    }
    if (x5c != null) {
        // make a copy before we start
        x5c = new ArrayList<>(x5c);
        if (rootCert != null) {
            x5c.add(rootCert);
        }
        CertificateHelper.checkValidity(x5c, includesRoot, options.getRootCrls());
    }
    return null;
}
Also used : JsonArray(io.vertx.core.json.JsonArray) AttestationException(io.vertx.ext.auth.webauthn.impl.attestation.AttestationException) JsonObject(io.vertx.core.json.JsonObject) CertificateException(java.security.cert.CertificateException) Nullable(io.vertx.codegen.annotations.Nullable)

Aggregations

JsonObject (io.vertx.core.json.JsonObject)6 AttestationException (io.vertx.ext.auth.webauthn.impl.attestation.AttestationException)6 JsonParser (com.fasterxml.jackson.core.JsonParser)3 Buffer (io.vertx.core.buffer.Buffer)2 JsonArray (io.vertx.core.json.JsonArray)2 User (io.vertx.ext.auth.User)2 JWK (io.vertx.ext.auth.impl.jose.JWK)2 JWS (io.vertx.ext.auth.impl.jose.JWS)2 WebAuthnCredentials (io.vertx.ext.auth.webauthn.WebAuthnCredentials)2 Attestation (io.vertx.ext.auth.webauthn.impl.attestation.Attestation)2 ContextState (io.quarkus.arc.InjectableContext.ContextState)1 ManagedContext (io.quarkus.arc.ManagedContext)1 SecurityIdentity (io.quarkus.security.identity.SecurityIdentity)1 RestoreResult (io.quarkus.vertx.http.runtime.security.PersistentLoginManager.RestoreResult)1 Nullable (io.vertx.codegen.annotations.Nullable)1 AsyncResult (io.vertx.core.AsyncResult)1 Future (io.vertx.core.Future)1 Handler (io.vertx.core.Handler)1 Vertx (io.vertx.core.Vertx)1 Logger (io.vertx.core.impl.logging.Logger)1