From ab6970135850655313e257cf44fb68c67e9f1e80 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Tue, 2 Aug 2022 19:16:11 -0700 Subject: New API token system --- .../org/traccar/api/resource/SessionResource.java | 30 ++++++++-- .../org/traccar/api/security/LoginService.java | 13 ++++- .../api/security/SecurityRequestFilter.java | 4 +- .../org/traccar/api/signature/CryptoManager.java | 25 +++++---- .../org/traccar/api/signature/TokenManager.java | 64 ++++++++++++++++++++++ src/main/java/org/traccar/model/User.java | 17 ------ 6 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 src/main/java/org/traccar/api/signature/TokenManager.java (limited to 'src') diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 8eabdc63c..f70b67cde 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 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. @@ -17,6 +17,7 @@ package org.traccar.api.resource; import org.traccar.api.BaseResource; import org.traccar.api.security.LoginService; +import org.traccar.api.signature.TokenManager; import org.traccar.helper.DataConverter; import org.traccar.helper.ServletHelper; import org.traccar.helper.LogAction; @@ -40,12 +41,16 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.io.UnsupportedEncodingException; +import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Date; +import java.util.concurrent.TimeUnit; @Path("session") @Produces(MediaType.APPLICATION_JSON) @@ -59,12 +64,15 @@ public class SessionResource extends BaseResource { @Inject private LoginService loginService; - @javax.ws.rs.core.Context + @Inject + private TokenManager tokenManager; + + @Context private HttpServletRequest request; @PermitAll @GET - public User get(@QueryParam("token") String token) throws StorageException, UnsupportedEncodingException { + public User get(@QueryParam("token") String token) throws StorageException, IOException, GeneralSecurityException { if (token != null) { User user = loginService.login(token); @@ -84,11 +92,11 @@ public class SessionResource extends BaseResource { for (Cookie cookie : cookies) { if (cookie.getName().equals(USER_COOKIE_KEY)) { byte[] emailBytes = DataConverter.parseBase64( - URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII.name())); + URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII)); email = new String(emailBytes, StandardCharsets.UTF_8); } else if (cookie.getName().equals(PASS_COOKIE_KEY)) { byte[] passwordBytes = DataConverter.parseBase64( - URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII.name())); + URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII)); password = new String(passwordBytes, StandardCharsets.UTF_8); } } @@ -144,4 +152,14 @@ public class SessionResource extends BaseResource { return Response.noContent().build(); } + @Path("token") + @POST + public String requestToken( + @FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException { + if (expiration == null) { + expiration = new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7)); + } + return tokenManager.generateToken(getUserId(), expiration); + } + } diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java index 104a6fac3..1e82a4cf2 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 org.traccar.api.signature.TokenManager; import org.traccar.config.Config; import org.traccar.config.Keys; import org.traccar.database.LdapProvider; @@ -27,29 +28,35 @@ import org.traccar.storage.query.Request; import javax.annotation.Nullable; import javax.inject.Inject; +import java.io.IOException; +import java.security.GeneralSecurityException; public class LoginService { private final Storage storage; + private final TokenManager tokenManager; private final LdapProvider ldapProvider; private final String serviceAccountToken; private final boolean forceLdap; @Inject - public LoginService(Config config, Storage storage, @Nullable LdapProvider ldapProvider) { + public LoginService( + Config config, Storage storage, TokenManager tokenManager, @Nullable LdapProvider ldapProvider) { this.storage = storage; + this.tokenManager = tokenManager; this.ldapProvider = ldapProvider; serviceAccountToken = config.getString(Keys.WEB_SERVICE_ACCOUNT_TOKEN); forceLdap = config.getBoolean(Keys.LDAP_FORCE); } - public User login(String token) throws StorageException { + public User login(String token) throws StorageException, GeneralSecurityException, IOException { if (serviceAccountToken != null && serviceAccountToken.equals(token)) { return new ServiceAccountUser(); } + long userId = tokenManager.verifyToken(token); User user = storage.getObject(User.class, new Request( - new Columns.All(), new Condition.Equals("token", "token", token))); + new Columns.All(), new Condition.Equals("id", "id", userId))); if (user != null) { checkUserEnabled(user); } diff --git a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index ada7bf997..94b6bbf05 100644 --- a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -33,8 +33,10 @@ import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; +import java.io.IOException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; public class SecurityRequestFilter implements ContainerRequestFilter { @@ -94,7 +96,7 @@ public class SecurityRequestFilter implements ContainerRequestFilter { statisticsManager.registerRequest(user.getId()); securityContext = new UserSecurityContext(new UserPrincipal(user.getId())); } - } catch (StorageException e) { + } catch (StorageException | GeneralSecurityException | IOException e) { throw new WebApplicationException(e); } diff --git a/src/main/java/org/traccar/api/signature/CryptoManager.java b/src/main/java/org/traccar/api/signature/CryptoManager.java index ea59dcd70..8a3e7704c 100644 --- a/src/main/java/org/traccar/api/signature/CryptoManager.java +++ b/src/main/java/org/traccar/api/signature/CryptoManager.java @@ -15,7 +15,6 @@ */ package org.traccar.api.signature; -import org.traccar.helper.DataConverter; import org.traccar.storage.Storage; import org.traccar.storage.StorageException; import org.traccar.storage.query.Columns; @@ -46,28 +45,32 @@ public class CryptoManager { this.storage = storage; } - public String sign(String data) throws GeneralSecurityException, StorageException { + public byte[] sign(byte[] data) throws GeneralSecurityException, StorageException { if (privateKey == null) { initializeKeys(); } Signature signature = Signature.getInstance("SHA256withECDSA"); signature.initSign(privateKey); - signature.update(data.getBytes()); - return data + '.' + DataConverter.printBase64(signature.sign()); + signature.update(data); + byte[] block = signature.sign(); + byte[] combined = new byte[1 + block.length + data.length]; + combined[0] = (byte) block.length; + System.arraycopy(block, 0, combined, 1, block.length); + System.arraycopy(data, 0, combined, 1 + block.length, data.length); + return combined; } - public String verify(String data) throws GeneralSecurityException, StorageException { + public byte[] verify(byte[] data) throws GeneralSecurityException, StorageException { if (publicKey == null) { initializeKeys(); } Signature signature = Signature.getInstance("SHA256withECDSA"); signature.initVerify(publicKey); - - int delimiter = data.lastIndexOf('.'); - String originalData = data.substring(0, delimiter); - - signature.update(originalData.getBytes()); - if (!signature.verify(DataConverter.parseBase64(data.substring(delimiter + 1)))) { + int length = data[0]; + byte[] originalData = new byte[data.length - 1 - length]; + System.arraycopy(data, 1 + length, originalData, 0, originalData.length); + signature.update(originalData); + if (!signature.verify(data, 1, length)) { throw new SecurityException("Invalid signature"); } return originalData; diff --git a/src/main/java/org/traccar/api/signature/TokenManager.java b/src/main/java/org/traccar/api/signature/TokenManager.java new file mode 100644 index 000000000..3f39d5380 --- /dev/null +++ b/src/main/java/org/traccar/api/signature/TokenManager.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 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.signature; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Base64; +import org.traccar.storage.StorageException; + +import javax.inject.Inject; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Date; + +public class TokenManager { + + private final ObjectMapper objectMapper; + private final CryptoManager cryptoManager; + + public static class Data { + @JsonProperty("u") + private long userId; + @JsonProperty("e") + private Date expiration; + } + + @Inject + public TokenManager(ObjectMapper objectMapper, CryptoManager cryptoManager) { + this.objectMapper = objectMapper; + this.cryptoManager = cryptoManager; + } + + public String generateToken( + long userId, Date expiration) throws IOException, GeneralSecurityException, StorageException { + Data data = new Data(); + data.userId = userId; + data.expiration = expiration; + byte[] encoded = objectMapper.writeValueAsBytes(data); + return Base64.encodeBase64URLSafeString(cryptoManager.sign(encoded)); + } + + public long verifyToken(String token) throws IOException, GeneralSecurityException, StorageException { + byte[] encoded = cryptoManager.verify(Base64.decodeBase64(token)); + Data data = objectMapper.readValue(encoded, Data.class); + if (data.expiration.before(new Date())) { + throw new SecurityException("Token has expired"); + } + return data.userId; + } + +} diff --git a/src/main/java/org/traccar/model/User.java b/src/main/java/org/traccar/model/User.java index 3db20c753..53594fe07 100644 --- a/src/main/java/org/traccar/model/User.java +++ b/src/main/java/org/traccar/model/User.java @@ -208,23 +208,6 @@ public class User extends ExtendedModel implements UserRestrictions, Disableable this.deviceReadonly = deviceReadonly; } - private String token; - - public String getToken() { - return token; - } - - public void setToken(String token) { - if (token != null && !token.isEmpty()) { - if (!token.matches("^[a-zA-Z0-9-]{16,}$")) { - throw new IllegalArgumentException("Illegal token"); - } - this.token = token; - } else { - this.token = null; - } - } - private boolean limitCommands; @Override -- cgit v1.2.3