From 3296318dccfcc83cc99d6da58affe5ee8a46fedb Mon Sep 17 00:00:00 2001 From: e-macgregor <122734173+e-macgregor@users.noreply.github.com> Date: Sun, 29 Oct 2023 17:20:36 -0600 Subject: totp --- build.gradle | 1 + schema/changelog-5.10.xml | 17 +++++++++++++ schema/changelog-master.xml | 1 + .../org/traccar/api/resource/SessionResource.java | 20 ++++++++++++---- .../org/traccar/api/resource/UserResource.java | 16 +++++++++++++ .../api/security/CodeRequiredException.java | 22 +++++++++++++++++ .../org/traccar/api/security/LoginService.java | 28 +++++++++++++++------- .../api/security/SecurityRequestFilter.java | 2 +- src/main/java/org/traccar/config/Keys.java | 14 +++++++++++ src/main/java/org/traccar/model/User.java | 10 ++++++++ 10 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 schema/changelog-5.10.xml create mode 100644 src/main/java/org/traccar/api/security/CodeRequiredException.java diff --git a/build.gradle b/build.gradle index 2e8b557ba..75fe792b0 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,7 @@ dependencies { implementation "com.google.firebase:firebase-admin:9.2.0" implementation "com.nimbusds:oauth2-oidc-sdk:10.13.2" implementation "com.rabbitmq:amqp-client:5.18.0" + implementation "com.warrenstrange:googleauth:1.2.0" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testImplementation "org.mockito:mockito-core:5.4.0" diff --git a/schema/changelog-5.10.xml b/schema/changelog-5.10.xml new file mode 100644 index 000000000..63988b14a --- /dev/null +++ b/schema/changelog-5.10.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/schema/changelog-master.xml b/schema/changelog-master.xml index 331d5ec78..183b3fd93 100644 --- a/schema/changelog-master.xml +++ b/schema/changelog-master.xml @@ -40,5 +40,6 @@ + diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 3e738c15a..90f0ceade 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -16,6 +16,7 @@ package org.traccar.api.resource; import org.traccar.api.BaseResource; +import org.traccar.api.security.CodeRequiredException; import org.traccar.api.security.LoginService; import org.traccar.api.signature.TokenManager; import org.traccar.database.OpenIdProvider; @@ -108,7 +109,7 @@ public class SessionResource extends BaseResource { } } if (email != null && password != null) { - User user = loginService.login(email, password); + User user = loginService.login(email, password, null); if (user != null) { request.getSession().setAttribute(USER_ID_KEY, user.getId()); LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); @@ -142,8 +143,19 @@ public class SessionResource extends BaseResource { @PermitAll @POST public User add( - @FormParam("email") String email, @FormParam("password") String password) throws StorageException { - User user = loginService.login(email, password); + @FormParam("email") String email, + @FormParam("password") String password, + @FormParam("code") Integer code) throws StorageException { + User user; + try { + user = loginService.login(email, password, code); + } catch (CodeRequiredException e) { + Response response = Response + .status(Response.Status.UNAUTHORIZED) + .header("WWW-Authenticate", "TOTP") + .build(); + throw new WebApplicationException(response); + } if (user != null) { request.getSession().setAttribute(USER_ID_KEY, user.getId()); LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); @@ -171,7 +183,7 @@ public class SessionResource extends BaseResource { @PermitAll @Path("openid/auth") @GET - public Response openIdAuth() throws IOException { + public Response openIdAuth() { return Response.seeOther(openIdProvider.createAuthUri()).build(); } diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java index d73e8b6f5..99537f912 100644 --- a/src/main/java/org/traccar/api/resource/UserResource.java +++ b/src/main/java/org/traccar/api/resource/UserResource.java @@ -15,12 +15,14 @@ */ package org.traccar.api.resource; +import com.warrenstrange.googleauth.GoogleAuthenticator; import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Context; import org.traccar.api.BaseObjectResource; import org.traccar.config.Config; +import org.traccar.config.Keys; import org.traccar.helper.LogAction; import org.traccar.helper.model.UserUtil; import org.traccar.model.ManagedUser; @@ -96,6 +98,10 @@ public class UserResource extends BaseObjectResource { if (!permissionsService.getServer().getRegistration()) { throw new SecurityException("Registration disabled"); } + if (permissionsService.getServer().getBoolean(Keys.WEB_TOTP_FORCE.getKey()) + && entity.getTotpKey() == null) { + throw new SecurityException("One-time password key is required"); + } UserUtil.setUserDefaults(entity, config); } } @@ -128,4 +134,14 @@ public class UserResource extends BaseObjectResource { return response; } + @Path("totp") + @PermitAll + @POST + public String generateTotpKey() throws StorageException { + if (!permissionsService.getServer().getBoolean(Keys.WEB_TOTP_ENABLE.getKey())) { + throw new SecurityException("One-time password is disabled"); + } + return new GoogleAuthenticator().createCredentials().getKey(); + } + } diff --git a/src/main/java/org/traccar/api/security/CodeRequiredException.java b/src/main/java/org/traccar/api/security/CodeRequiredException.java new file mode 100644 index 000000000..d522c6540 --- /dev/null +++ b/src/main/java/org/traccar/api/security/CodeRequiredException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Anton Tananaev (anton@traccar.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.traccar.api.security; + +public class CodeRequiredException extends SecurityException { + public CodeRequiredException() { + super("Code not provided"); + } +} diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java index 91e964ee9..8eb5537fa 100644 --- a/src/main/java/org/traccar/api/security/LoginService.java +++ b/src/main/java/org/traccar/api/security/LoginService.java @@ -15,6 +15,7 @@ */ package org.traccar.api.security; +import com.warrenstrange.googleauth.GoogleAuthenticator; import org.traccar.api.signature.TokenManager; import org.traccar.config.Config; import org.traccar.config.Keys; @@ -70,7 +71,7 @@ public class LoginService { return user; } - public User login(String email, String password) throws StorageException { + public User login(String email, String password, Integer code) throws StorageException { if (forceOpenId) { return null; } @@ -84,6 +85,7 @@ public class LoginService { if (user != null) { if (ldapProvider != null && user.getLogin() != null && ldapProvider.login(user.getLogin(), password) || !forceLdap && user.isPasswordValid(password)) { + checkUserCode(user, code); checkUserEnabled(user); return user; } @@ -98,15 +100,12 @@ public class LoginService { return null; } - public User login(String email, String name, Boolean administrator) throws StorageException { + public User login(String email, String name, boolean administrator) throws StorageException { User user = storage.getObject(User.class, new Request( new Columns.All(), new Condition.Equals("email", email))); - if (user != null) { - checkUserEnabled(user); - return user; - } else { + if (user == null) { user = new User(); UserUtil.setUserDefaults(user, config); user.setName(name); @@ -114,9 +113,9 @@ public class LoginService { user.setFixedEmail(true); user.setAdministrator(administrator); user.setId(storage.addObject(user, new Request(new Columns.Exclude("id")))); - checkUserEnabled(user); - return user; } + checkUserEnabled(user); + return user; } private void checkUserEnabled(User user) throws SecurityException { @@ -126,4 +125,17 @@ public class LoginService { user.checkDisabled(); } + private void checkUserCode(User user, Integer code) throws SecurityException { + String key = user.getTotpKey(); + if (key != null) { + if (code == null) { + throw new CodeRequiredException(); + } + GoogleAuthenticator authenticator = new GoogleAuthenticator(); + if (!authenticator.authorize(key, code)) { + throw new SecurityException("User authorization failed"); + } + } + } + } diff --git a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index ee964c9e4..cb523177e 100644 --- a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -87,7 +87,7 @@ public class SecurityRequestFilter implements ContainerRequestFilter { user = loginService.login(authHeader.substring(7)); } else { String[] auth = decodeBasicAuth(authHeader); - user = loginService.login(auth[0], auth[1]); + user = loginService.login(auth[0], auth[1], null); } if (user != null) { statisticsManager.registerRequest(user.getId()); diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java index 91063a8e0..48dec863d 100644 --- a/src/main/java/org/traccar/config/Keys.java +++ b/src/main/java/org/traccar/config/Keys.java @@ -837,6 +837,20 @@ public final class Keys { List.of(KeyType.CONFIG), "max-age=3600,public"); + /** + * Enable TOTP authentication on the server. + */ + public static final ConfigKey WEB_TOTP_ENABLE = new BooleanConfigKey( + "totpEnable", + List.of(KeyType.SERVER)); + + /** + * Server attribute that indicates that TOTP authentication is required for new users. + */ + public static final ConfigKey WEB_TOTP_FORCE = new BooleanConfigKey( + "totpForce", + List.of(KeyType.SERVER)); + /** * Host for raw data forwarding. */ diff --git a/src/main/java/org/traccar/model/User.java b/src/main/java/org/traccar/model/User.java index 0540f16d7..757064ba2 100644 --- a/src/main/java/org/traccar/model/User.java +++ b/src/main/java/org/traccar/model/User.java @@ -251,6 +251,16 @@ public class User extends ExtendedModel implements UserRestrictions, Disableable this.poiLayer = poiLayer; } + private String totpKey; + + public String getTotpKey() { + return totpKey; + } + + public void setTotpKey(String totpKey) { + this.totpKey = totpKey; + } + @QueryIgnore public String getPassword() { return null; -- cgit v1.2.3