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);
}
}
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);
}
});
}
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();
}
}
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));
}
}
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;
}
Aggregations