@@ -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) | |||
@@ -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(); } | |||
} |
@@ -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}") | |||
@@ -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)); | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -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 | |||
@@ -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 | |||
@@ -1 +1 @@ | |||
Subproject commit b87ba60f28f565f2e32709e73bacbf847ccef29c | |||
Subproject commit 7fe91bd61d92d8b70115f2dd7c5fd24c6a7410b6 |