diff --git a/bubble-server/src/main/java/bubble/model/account/AccountContact.java b/bubble-server/src/main/java/bubble/model/account/AccountContact.java index 3d47fc60..c8595b74 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountContact.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountContact.java @@ -54,6 +54,7 @@ public class AccountContact implements Serializable { @HasValue(message="err.info.required") @Getter @Setter private String info; + @JsonIgnore public String getTotpKey () { return totpInfo().getKey(); } public TotpBean totpInfo () { return !empty(info) && isAuthenticator() ? json(info, TotpBean.class) : null; } @HasValue(message="err.cloudServiceType.required") @@ -93,7 +94,7 @@ public class AccountContact implements Serializable { if (c.isAuthenticator()) { final AccountContact auth = findAuthenticator(contacts); - if (auth != null && !auth.getUuid().equals(c.getUuid())) { + if (auth != null && (!c.hasUuid() || !auth.getUuid().equals(c.getUuid()))) { throw invalidEx("err.authenticator.configured", "Only one authenticator can be configured"); } } diff --git a/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java b/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java index bd9ad76a..859369a7 100644 --- a/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java +++ b/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java @@ -5,11 +5,16 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import static org.cobbzilla.util.string.StringUtil.safeParseInt; + @NoArgsConstructor @Accessors(chain=true) public class AuthenticatorRequest { @Getter @Setter private String account; - @Getter @Setter private int token; + + @Getter @Setter private String token; + public Integer intToken() { return safeParseInt(getToken()); } + @Getter @Setter private Boolean verify; public boolean verify() { return verify != null && verify; } 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 73957428..457fb15a 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java @@ -148,7 +148,9 @@ public class AccountsResource { final AccountContact existing = policy.findContact(contact); if (existing != null) { - if (existing.isAuthenticator()) return invalid("err.authenticator.configured"); + if (existing.isAuthenticator() && (!contact.hasUuid() || !existing.getUuid().equals(contact.getUuid()))) { + return invalid("err.authenticator.configured"); + } // if it already exists, these fields cannot be changed contact.setUuid(existing.getUuid()); @@ -210,6 +212,16 @@ public class AccountsResource { return ok(policyDAO.update(policy.removeContact(contact)).mask()); } + @DELETE @Path("/{id}"+EP_POLICY+EP_CONTACTS+EP_AUTHENTICATOR) + public Response removeAuthenticator(@Context ContainerRequest ctx, + @PathParam("id") String id) { + final AccountContext c = new AccountContext(ctx, id); + final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + 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()); + } + @DELETE @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/{uuid}") public Response removeContactByUuid(@Context ContainerRequest ctx, @PathParam("id") String id, 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 ab8a834c..1284ea04 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -300,7 +300,9 @@ public class AuthResource { if (authenticator == null) return invalid("err.authenticator.notConfigured"); final String secret = authenticator.totpInfo().getKey(); - if (G_AUTH.authorize(secret, request.getToken())) { + final Integer code = request.intToken(); + if (code == null) return invalid("err.token.invalid"); + if (G_AUTH.authorize(secret, code)) { if (request.verify()) { policyDAO.update(policy.verifyContact(policy.getAuthenticator().getUuid())); return ok_empty(); diff --git a/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java b/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java index 28b6eea4..b98729f6 100644 --- a/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java +++ b/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java @@ -197,6 +197,7 @@ public class StandardAccountMessageService implements AccountMessageService { AccountMessageType type) { return captureResponse(account, remoteHost, token, type, null); } + public AccountMessage captureResponse(Account account, String remoteHost, String token, diff --git a/bubble-server/src/test/resources/models/tests/auth/forgot_password.json b/bubble-server/src/test/resources/models/tests/auth/forgot_password.json index 70f1b01b..bba93ac7 100644 --- a/bubble-server/src/test/resources/models/tests/auth/forgot_password.json +++ b/bubble-server/src/test/resources/models/tests/auth/forgot_password.json @@ -3,6 +3,8 @@ "comment": "activate service, create account, login", "include": "new_account", "params": { + "username": "user-forgot_password", + "email": "user-forgot_password@example.com", "password": "foobar", "verifyEmail": "true" } @@ -17,6 +19,18 @@ "info": "US:800-555-1212" } }, + "response": { + "store": "policy", + "check": [ + {"condition": "json.getType().name() == 'sms'"}, + {"condition": "json.getInfo() == 'US:800-555-1212'"} + ] + } + }, + + { + "comment": "re-read user policy", + "request": { "uri": "users/{{userAccount.name}}/policy" }, "response": { "store": "policy", "check": [ @@ -48,11 +62,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", - "method": "post" - }, - "response": { - "sessionName": "userSession", - "session": "token" + "entity": [{"name": "account", "value": "user-forgot_password"}] } }, @@ -74,7 +84,7 @@ "comment": "as root, check email inbox, expect reset password request", "request": { "session": "rootSession", - "uri": "debug/inbox/email/{{policy.firstEmail}}?action=password" + "uri": "debug/inbox/email/user-forgot_password@example.com?action=password" }, "response": { "store": "emailInbox", @@ -109,7 +119,10 @@ "request": { "session": "new", "uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}", - "entity": [{ "name": "password", "value": "new_password" }] + "entity": [ + { "name": "account", "value": "user-forgot_password" }, + { "name": "password", "value": "new_password" } + ] }, "response": { "status": 422, @@ -122,7 +135,10 @@ "request": { "session": "new", "uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}", - "entity": [{ "name": "password", "value": "new_password1!" }] + "entity": [ + { "name": "account", "value": "user-forgot_password" }, + { "name": "password", "value": "new_password1!" } + ] } }, @@ -131,7 +147,10 @@ "request": { "session": "new", "uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", - "entity": [{ "name": "password", "value": "another_new_password1!" }] + "entity": [ + { "name": "account", "value": "user-forgot_password" }, + { "name": "password", "value": "another_new_password1!" } + ] }, "response": { "status": 422, diff --git a/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json b/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json index 64170db5..144a5e83 100644 --- a/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json +++ b/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json @@ -3,6 +3,8 @@ "comment": "activate service, create account, login", "include": "new_account", "params": { + "username": "user-multifactor_auth", + "email": "user-multifactor_auth@example.com", "password": "foobar1!" } }, @@ -27,7 +29,7 @@ "uri": "users/{{userAccount.name}}/policy/contacts", "entity": { "type": "email", - "info": "{{policy.firstEmail}}", + "info": "user-multifactor_auth@example.com", "authFactor": "required" } }, @@ -45,7 +47,7 @@ "uri": "users/{{userAccount.name}}/policy/contacts/verify", "entity": { "type": "email", - "info": "{{policy.firstEmail}}" + "info": "user-multifactor_auth@example.com" } } }, @@ -55,7 +57,7 @@ "comment": "as root, check inbox (1st) for email verification message", "request": { "session": "rootSession", - "uri": "debug/inbox/email/{{policy.firstEmail}}?action=verify" + "uri": "debug/inbox/email/user-multifactor_auth@example.com?action=verify" }, "response": { "store": "userInbox", @@ -72,11 +74,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", - "method": "post" - }, - "response": { - "sessionName": "userSession", - "session": "token" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] } }, @@ -102,10 +100,22 @@ "uri": "users/{{userAccount.name}}/policy/contacts", "entity": { "type": "email", - "info": "{{policy.firstEmail}}", + "info": "user-multifactor_auth@example.com", "authFactor": "required" } }, + "response": { + "check": [ + {"condition": "json.getType().name() == 'email'"}, + {"condition": "json.getInfo() == 'user-multifactor_auth@example.com'"}, + {"condition": "json.getAuthFactor().name() == 'required'"} + ] + } + }, + + { + "comment": "re-read policy, verify email is now a required auth factor", + "request": { "uri": "users/{{userAccount.name}}/policy" }, "response": { "store": "policy", "check": [ @@ -135,6 +145,7 @@ "check": [ {"condition": "json.getMultifactorAuth() != null"}, {"condition": "json.getMultifactorAuth().length == 1"}, + {"condition": "json.getMultifactorAuth()[0].getType().name() == 'email'"}, {"condition": "json.getMultifactorAuth()[0].getInfo().indexOf('***') != -1"} ] } @@ -145,7 +156,7 @@ "comment": "as root, check inbox (2nd), verify login request message sent", "request": { "session": "rootSession", - "uri": "debug/inbox/email/{{policy.firstEmail}}?action=login" + "uri": "debug/inbox/email/user-multifactor_auth@example.com?action=login" }, "response": { "store": "userInbox", @@ -162,7 +173,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", - "method": "post" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] }, "response": { "sessionName": "userSession", @@ -178,6 +189,46 @@ "type": "authenticator" } }, + "response": { + "store": "authenticator", + "check": [ + {"condition": "json.getType().name() == 'authenticator'"} + ] + } + }, + + { + "comment": "verify authenticator", + "request": { + "uri": "auth/authenticator", + "entity": { + "account": "{{userAccount.name}}", + "token": "{{authenticator_token authenticator.totpKey}}", + "verify": true + } + } + }, + + { + "comment": "set authenticator as required auth factor", + "request": { + "uri": "users/{{userAccount.name}}/policy/contacts", + "data": "authenticator", + "entity": { + "authFactor": "required" + } + }, + "response": { + "store": "authenticator", + "check": [ + {"condition": "json.getType().name() == 'authenticator'"} + ] + } + }, + + { + "comment": "re-read policy, both contacts are now required auth factors", + "request": { "uri": "users/{{userAccount.name}}/policy" }, "response": { "store": "policy", "check": [ @@ -209,7 +260,7 @@ {"condition": "json.getMultifactorAuth() != null"}, {"condition": "json.getMultifactorAuth().length == 2"}, {"condition": "json.getMultifactorAuth()[0].getInfo().indexOf('***') != -1"}, - {"condition": "json.getMultifactorAuth()[1].getInfo().indexOf('***') != -1"} + {"condition": "json.getMultifactorAuth()[1].getInfo() == '{\"masked\": true}'"} ] } }, @@ -219,7 +270,7 @@ "comment": "as root, check inbox (3rd), verify login request message sent", "request": { "session": "rootSession", - "uri": "debug/inbox/email/{{policy.firstEmail}}?action=login" + "uri": "debug/inbox/email/user-multifactor_auth@example.com?action=login" }, "response": { "store": "userInbox", @@ -236,7 +287,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", - "method": "post" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] }, "response": { "store": "remainingApprovals", @@ -244,7 +295,7 @@ {"condition": "json.getUuid() == null"}, {"condition": "json.getMultifactorAuth() != null"}, {"condition": "json.getMultifactorAuth().length == 1"}, - {"condition": "json.getMultifactorAuth()[0].getInfo().indexOf('***') != -1"} + {"condition": "json.getMultifactorAuth()[0].getInfo() == '{\"masked\": true}'"} ] } }, @@ -255,7 +306,7 @@ "uri": "auth/authenticator", "entity": { "account": "{{userAccount.name}}", - "token": "{{authenticator_token policy.authenticator.info}}" + "token": "{{authenticator_token authenticator.totpKey}}" } }, "response": { @@ -265,9 +316,9 @@ }, { - "comment": "remove email as required auth factor", + "comment": "remove email from contacts", "request": { - "uri": "users/{{userAccount.name}}/policy/contacts/email/{{policy.firstEmail}}", + "uri": "users/{{userAccount.name}}/policy/contacts/email/user-multifactor_auth@example.com", "method": "delete" }, "response": { @@ -308,10 +359,10 @@ } }, "response": { - "store": "policy", + "store": "smsContact", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 2"} + {"condition": "json.getType().name() == 'sms'"}, + {"condition": "json.getAuthFactor().name() == 'not_required'"} ] } }, @@ -338,11 +389,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", - "method": "post" - }, - "response": { - "sessionName": "userSession", - "session": "token" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] } }, @@ -369,17 +416,14 @@ "uri": "users/{{userAccount.name}}/policy/contacts", "entity": { "type": "sms", - "info": "{{policy.firstSms}}", + "info": "{{smsContact.info}}", "authFactor": "sufficient" } }, "response": { - "store": "policy", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 2"}, - {"condition": "json.getAccountContacts()[1].sufficientAuthFactor()"}, - {"condition": "json.getAccountContacts()[1].getType().name() == 'sms'"} + {"condition": "json.getType().name() == 'sms'"}, + {"condition": "json.getAuthFactor().name() == 'sufficient'"} ] } }, @@ -404,7 +448,7 @@ "check": [ {"condition": "json.getMultifactorAuth() != null"}, {"condition": "json.getMultifactorAuth().length == 2"}, - {"condition": "json.getMultifactorAuth()[0].getInfo().indexOf('***') != -1"}, + {"condition": "json.getMultifactorAuth()[0].getInfo() == '{\"masked\": true}'"}, {"condition": "json.getMultifactorAuth()[1].getInfo().indexOf('***') != -1"} ] } @@ -432,7 +476,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", - "method": "post" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] }, "response": { "store": "remainingApprovals", @@ -440,7 +484,7 @@ {"condition": "json.getUuid() == null"}, {"condition": "json.getMultifactorAuth() != null"}, {"condition": "json.getMultifactorAuth().length == 1"}, - {"condition": "json.getMultifactorAuth()[0].getInfo().indexOf('***') != -1"} + {"condition": "json.getMultifactorAuth()[0].getInfo() == '{\"masked\": true}'"} ] } }, @@ -451,7 +495,7 @@ "uri": "auth/authenticator", "entity": { "account": "{{userAccount.name}}", - "token": "{{authenticator_token policy.authenticator.info}}" + "token": "{{authenticator_token authenticator.totpKey}}" } }, "response": { @@ -461,7 +505,7 @@ }, { - "comment": "remove authenticator", + "comment": "remove authenticator, only SMS remains as contact method", "request": { "uri": "users/{{userAccount.name}}/policy/contacts/authenticator", "method": "delete" @@ -470,7 +514,8 @@ "store": "policy", "check": [ {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 1"} + {"condition": "json.getAccountContacts().length == 1"}, + {"condition": "json.getAccountContacts()[0].getType().name() == 'sms'"} ] } }, @@ -485,10 +530,9 @@ } }, "response": { - "store": "policy", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 2"} + {"condition": "json.getType().name() == 'email'"}, + {"condition": "json.getInfo() == 'foo@example.com'"} ] } }, @@ -515,11 +559,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", - "method": "post" - }, - "response": { - "sessionName": "userSession", - "session": "token" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] } }, @@ -535,10 +575,9 @@ } }, "response": { - "store": "policy", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 2"} + {"condition": "json.getAuthFactor().name() == 'sufficient'"}, + {"condition": "json.getInfo() == 'foo@example.com'"} ] } }, @@ -573,7 +612,7 @@ "comment": "as root, check inbox (7th), verify login request message sent", "request": { "session": "rootSession", - "uri": "debug/inbox/email/{{policy.firstEmail}}?action=login" + "uri": "debug/inbox/email/foo@example.com?type=request&action=login&target=account" }, "response": { "store": "userInbox", @@ -590,7 +629,7 @@ "request": { "session": "userSession", "uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", - "method": "post" + "entity": [{"name": "account", "value": "user-multifactor_auth"}] }, "response": { "sessionName": "userSession", @@ -609,10 +648,9 @@ } }, "response": { - "store": "policy", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 2"} + {"condition": "json.getType().name() == 'email'"}, + {"condition": "json.getAuthFactor().name() == 'not_required'"} ] } }, @@ -623,15 +661,14 @@ "uri": "users/{{userAccount.name}}/policy/contacts", "entity": { "type": "sms", - "info": "US:800-555-1212", + "info": "{{smsContact.info}}", "authFactor": "not_required" } }, "response": { - "store": "policy", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 2"} + {"condition": "json.getType().name() == 'sms'"}, + {"condition": "json.getAuthFactor().name() == 'not_required'"} ] } }, @@ -669,8 +706,8 @@ "response": { "store": "policy", "check": [ - {"condition": "json.getAccountContacts() != null"}, - {"condition": "json.getAccountContacts().length == 3"} + {"condition": "json.getInfo() == 'bar@example.com'"}, + {"condition": "json.getType().name() == 'email'"} ] } }, diff --git a/bubble-web b/bubble-web index 2d43a115..8ccb01dd 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit 2d43a115e364e3c886dfde195339572eb079880e +Subproject commit 8ccb01dd1484e42243ecf2598c2b8c257fec1b9b