From f32da981e06ca80dc557250efa9e4ad15a6c4bd5 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Sun, 12 Jan 2020 16:40:16 -0500 Subject: [PATCH] support authenticator config in ui --- .../bubble/model/account/AccountPolicy.java | 7 ++++++- .../message/AccountMessageContact.java | 2 ++ .../resources/account/AccountsResource.java | 16 +++++++++++++--- .../resources/account/AuthResource.java | 16 ++++++++++++++++ .../bubble/service/AuthenticatorService.java | 19 +++++++++++++++++-- .../post_auth/ResourceMessages.properties | 5 ++++- .../pre_auth/ResourceMessages.properties | 6 ++++++ bubble-web | 2 +- 8 files changed, 65 insertions(+), 8 deletions(-) diff --git a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java index 695fc906..9c3ec346 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java @@ -46,7 +46,9 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { public static final Long MIN_NODE_OPERATION_TIMEOUT = MINUTES.toMillis(1); public static final Long MIN_AUTHENTICATOR_TIMEOUT = MINUTES.toMillis(1); - public static final String[] UPDATE_FIELDS = {"deletionPolicy", "nodeOperationTimeout", "accountOperationTimeout"}; + public static final String[] UPDATE_FIELDS = { + "deletionPolicy", "nodeOperationTimeout", "accountOperationTimeout", "authenticatorTimeout" + }; public AccountPolicy(AccountPolicy policy) { copy(this, policy); } @@ -70,6 +72,9 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { @ECSearchable(type=EntityFieldType.time_duration) @ECField(index=40) @Type(type=ENCRYPTED_LONG) @Column(columnDefinition="varchar("+ENC_LONG+") NOT NULL") @Getter @Setter private Long authenticatorTimeout = MAX_AUTHENTICATOR_TIMEOUT; + public boolean authenticatorTimeoutChanged (AccountPolicy other) { + return authenticatorTimeout != null && other.getAuthenticatorTimeout() != null && !authenticatorTimeout.equals(other.getAuthenticatorTimeout()); + } @ECSearchable @ECField(index=50) @Enumerated(EnumType.STRING) @Column(length=40, nullable=false) diff --git a/bubble-server/src/main/java/bubble/model/account/message/AccountMessageContact.java b/bubble-server/src/main/java/bubble/model/account/message/AccountMessageContact.java index c991abbe..6c1b0676 100644 --- a/bubble-server/src/main/java/bubble/model/account/message/AccountMessageContact.java +++ b/bubble-server/src/main/java/bubble/model/account/message/AccountMessageContact.java @@ -15,6 +15,8 @@ public class AccountMessageContact implements Serializable { @Getter @Setter private AccountMessage message; @Getter @Setter private AccountContact contact; + public boolean valid () { return message != null && message.hasUuid() && contact != null && contact.hasUuid(); } + public String key() { return message.getUuid()+":"+contact.getUuid(); } } diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java index e2ea3d3b..ca1ba0ef 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java @@ -139,6 +139,7 @@ public class AccountsResource { @PathParam("id") String id) { final AccountContext c = new AccountContext(ctx, id); final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); return policy == null ? notFound(id) : ok(policy.mask()); } @@ -152,8 +153,12 @@ public class AccountsResource { policy = policyDAO.create(new AccountPolicy(request).setAccount(c.account.getUuid())); } else { authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); + if (policy.authenticatorTimeoutChanged(request)) { + authenticatorService.updateExpiration(ctx, policy); + } policy = policyDAO.update((AccountPolicy) policy.update(request)); } + return ok(policy.mask()); } @@ -184,7 +189,7 @@ public class AccountsResource { log.info("setContact: contact is new, sending verify message"); messageDAO.sendVerifyRequest(getRemoteHost(req), c.account, contact); } - return ok(added); + return ok(added.mask()); } @POST @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/verify") @@ -201,7 +206,7 @@ public class AccountsResource { final AccountContact found = policy.findContact(contact); if (found == null) return notFound(contact.getType()+"/"+contact.getInfo()); messageDAO.sendVerifyRequest(getRemoteHost(req), c.account, found); - return ok(policy); + return ok(policy.mask()); } @GET @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/{type}/{info}") @@ -211,6 +216,7 @@ public class AccountsResource { @PathParam("info") String info) { final AccountContext c = new AccountContext(ctx, id); final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); return contact != null ? ok(contact.mask()) : notFound(type+"/"+info); } @@ -239,7 +245,11 @@ public class AccountsResource { final AccountContact contact = policy.findContact(new AccountContact().setType(CloudServiceType.authenticator)); if (contact == null) return notFound(CloudServiceType.authenticator.name()); - return ok(policyDAO.update(policy.removeContact(contact)).mask()); + + final AccountPolicy updated = policyDAO.update(policy.removeContact(contact)).mask(); + authenticatorService.flush(c.caller.getToken()); + + return ok(updated); } @DELETE @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/{uuid}") diff --git a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java index 264c4bf6..4d3c75f5 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -322,9 +322,25 @@ public class AuthResource { policyDAO.update(policy.verifyContact(policy.getAuthenticator().getUuid())); return ok_empty(); } + final AccountMessage loginRequest = accountMessageDAO.findMostRecentLoginRequest(account.getUuid()); + if (loginRequest == null) { + log.warn("authenticator: AccountMessage (loginRequest) was null, returning OK without doing anything further"); + return ok_empty(); + } + final AccountMessageContact amc = messageService.accountMessageContact(loginRequest, authenticator); + if (amc == null || !amc.valid()) { + log.warn("authenticator: AccountMessageContact was null or invalid, returning OK without doing anything further"); + return ok_empty(); + } + final AccountMessage approval = messageService.approve(account, getRemoteHost(req), amc.key()); + if (approval == null) { + log.warn("authenticator: AccountMessage (approval) was null, returning OK without doing anything further"); + return ok_empty(); + } + if (approval.getMessageType() == AccountMessageType.confirmation) { // OK we can log in! return ok(account.setToken(sessionToken)); diff --git a/bubble-server/src/main/java/bubble/service/AuthenticatorService.java b/bubble-server/src/main/java/bubble/service/AuthenticatorService.java index 4f664cea..53ba6b8a 100644 --- a/bubble-server/src/main/java/bubble/service/AuthenticatorService.java +++ b/bubble-server/src/main/java/bubble/service/AuthenticatorService.java @@ -38,7 +38,7 @@ public class AuthenticatorService { if (G_AUTH.authorize(secret, code)) { final String sessionToken = request.startSession() ? sessionDAO.create(account) : account.getToken(); if (sessionToken == null) throw invalidEx("err.totpToken.noSession"); - getAuthenticatorTimes().set(sessionToken, String.valueOf(now()), EX, policy.getAuthenticatorTimeout()/1000); + markAsAuthenticated(sessionToken, policy); return sessionToken; } else { @@ -46,6 +46,10 @@ public class AuthenticatorService { } } + public void markAsAuthenticated(String sessionToken, AccountPolicy policy) { + getAuthenticatorTimes().set(sessionToken, String.valueOf(now()), EX, policy.getAuthenticatorTimeout()/1000); + } + public boolean isAuthenticated (String sessionToken) { return getAuthenticatorTimes().get(sessionToken) != null; } public void ensureAuthenticated(ContainerRequest ctx) { ensureAuthenticated(ctx, null); } @@ -74,6 +78,17 @@ public class AuthenticatorService { if (!isAuthenticated(account.getToken())) throw invalidEx("err.totpToken.invalid"); } - public void flush(String sessionToken) { getAuthenticatorTimes().del(sessionToken); } + public boolean flush(String sessionToken) { + final String exists = getAuthenticatorTimes().get(sessionToken); + getAuthenticatorTimes().del(sessionToken); + return exists != null; + } + public void updateExpiration(ContainerRequest ctx, AccountPolicy policy) { + final Account account = userPrincipal(ctx); + final String sessionToken = account.getToken(); + if (flush(sessionToken)) { + markAsAuthenticated(sessionToken, policy); + } + } } diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties index e3d214a2..e48cfa6e 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties @@ -97,6 +97,8 @@ field_label_policy_account_operation_timeout=Account Operation Timeout field_label_policy_account_operation_timeout_description=To ensure your Account security, certain operations (like downloading your Account data) require your approval. If no response is received before this timeout, the operation will not be allowed to proceed. field_label_policy_node_operation_timeout=Bubble Operation Timeout field_label_policy_node_operation_timeout_description=To ensure your Bubble security, certain operations (like stopping your Bubble) require your approval. If no response is received before this timeout, the operation will not be allowed to proceed. +field_label_policy_authenticator_timeout=Authenticator Timeout +field_label_policy_authenticator_timeout_description=After successfully verifying your account with your Authenticator app, you will be required to authenticate again after this amount of time has passed. button_label_update_policy=Update # Policy Contact fields @@ -367,7 +369,8 @@ err.cloudType.invalid=Cloud type is invalid err.configJson.length=Configuration JSON is too long err.contact.required=Contact information is required err.contactType.required=Contact type is required -err.contact.unverified=Cannot set auth factor on an unverified contact; verify first +err.contact.authenticatorAuthFactorCannotBeChanged=Authenticator is always a required auth factor +err.contact.unverified=Cannot configure an unverified contact; verify first err.country.invalid=Country is invalid, use a valid ISO 2-letter code err.credentialsJson.length=Credentials JSON is too long err.data.length=Data is too long diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties index 8fc5413a..998e9fae 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties @@ -194,6 +194,12 @@ field_label_policy_contact_verified=Verified field_label_policy_contact_verify_code=Enter Verification Code button_label_submit_verify_code=Verify +# TOTP modal +form_title_totp_modal=Authentication Required +field_description_totp_code=Open your authentication app and enter the code for this service +field_label_totp_code=Enter 6 digit code +button_label_submit_totp_code=Verify + # Low-level errors and activation errors err.cloud.noSuchField=A cloud driver config field name is invalid err.cloud.invalidFieldType=Cloud driver config field type invalid diff --git a/bubble-web b/bubble-web index b87ba60f..7fe91bd6 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit b87ba60f28f565f2e32709e73bacbf847ccef29c +Subproject commit 7fe91bd61d92d8b70115f2dd7c5fd24c6a7410b6