aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnton Tananaev <anton.tananaev@gmail.com>2023-04-03 10:12:19 -0700
committerGitHub <noreply@github.com>2023-04-03 10:12:19 -0700
commit2d92fa2473b2317f01b904a8f1afd83e7884d7c8 (patch)
tree3ed8307268122fc23838f4c4ab97fb954c1c1370 /src
parent836fb2221dedae55c3f8457f35294b3753f095c8 (diff)
parentd05049c4fcad15b014d4d7178f3b88de7c0c7a28 (diff)
downloadtrackermap-server-2d92fa2473b2317f01b904a8f1afd83e7884d7c8.tar.gz
trackermap-server-2d92fa2473b2317f01b904a8f1afd83e7884d7c8.tar.bz2
trackermap-server-2d92fa2473b2317f01b904a8f1afd83e7884d7c8.zip
Merge pull request #5057 from dan-r/implement-oidc
Implement SSO with OpenID Connect
Diffstat (limited to 'src')
-rw-r--r--src/main/java/org/traccar/MainModule.java11
-rw-r--r--src/main/java/org/traccar/api/resource/ServerResource.java7
-rw-r--r--src/main/java/org/traccar/api/resource/SessionResource.java25
-rw-r--r--src/main/java/org/traccar/api/resource/UserResource.java8
-rw-r--r--src/main/java/org/traccar/api/security/LoginService.java24
-rw-r--r--src/main/java/org/traccar/config/Keys.java64
-rw-r--r--src/main/java/org/traccar/database/OpenIdProvider.java172
-rw-r--r--src/main/java/org/traccar/helper/model/UserUtil.java10
-rw-r--r--src/main/java/org/traccar/model/Server.java24
9 files changed, 337 insertions, 8 deletions
diff --git a/src/main/java/org/traccar/MainModule.java b/src/main/java/org/traccar/MainModule.java
index 663747de1..51097511a 100644
--- a/src/main/java/org/traccar/MainModule.java
+++ b/src/main/java/org/traccar/MainModule.java
@@ -33,6 +33,7 @@ import org.traccar.broadcast.NullBroadcastService;
import org.traccar.config.Config;
import org.traccar.config.Keys;
import org.traccar.database.LdapProvider;
+import org.traccar.database.OpenIdProvider;
import org.traccar.database.StatisticsManager;
import org.traccar.forward.EventForwarder;
import org.traccar.forward.EventForwarderJson;
@@ -87,6 +88,7 @@ import org.traccar.storage.DatabaseStorage;
import org.traccar.storage.MemoryStorage;
import org.traccar.storage.Storage;
import org.traccar.web.WebServer;
+import org.traccar.api.security.LoginService;
import javax.annotation.Nullable;
import javax.inject.Singleton;
@@ -170,6 +172,15 @@ public class MainModule extends AbstractModule {
return null;
}
+ @Singleton
+ @Provides
+ public static OpenIdProvider provideOpenIDProvider(Config config, LoginService loginService) {
+ if (config.hasKey(Keys.OPENID_CLIENTID)) {
+ return new OpenIdProvider(config, loginService);
+ }
+ return null;
+ }
+
@Provides
public static WebServer provideWebServer(Injector injector, Config config) {
if (config.hasKey(Keys.WEB_PORT)) {
diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java
index 4b7ee9189..6a3b8919e 100644
--- a/src/main/java/org/traccar/api/resource/ServerResource.java
+++ b/src/main/java/org/traccar/api/resource/ServerResource.java
@@ -16,6 +16,7 @@
package org.traccar.api.resource;
import org.traccar.api.BaseResource;
+import org.traccar.database.OpenIdProvider;
import org.traccar.helper.model.UserUtil;
import org.traccar.mail.MailManager;
import org.traccar.geocoder.Geocoder;
@@ -57,6 +58,10 @@ public class ServerResource extends BaseResource {
@Inject
@Nullable
+ private OpenIdProvider openIdProvider;
+
+ @Inject
+ @Nullable
private Geocoder geocoder;
@PermitAll
@@ -65,6 +70,8 @@ public class ServerResource extends BaseResource {
Server server = storage.getObject(Server.class, new Request(new Columns.All()));
server.setEmailEnabled(mailManager.getEmailEnabled());
server.setGeocoderEnabled(geocoder != null);
+ server.setOpenIdEnabled(openIdProvider != null);
+ server.setOpenIdForce(openIdProvider != null && openIdProvider.getForce());
User user = permissionsService.getUser(getUserId());
if (user != null) {
if (user.getAdministrator()) {
diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java
index ff84c135f..ac39fa449 100644
--- a/src/main/java/org/traccar/api/resource/SessionResource.java
+++ b/src/main/java/org/traccar/api/resource/SessionResource.java
@@ -18,6 +18,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.database.OpenIdProvider;
import org.traccar.helper.DataConverter;
import org.traccar.helper.LogAction;
import org.traccar.helper.ServletHelper;
@@ -27,6 +28,8 @@ import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Condition;
import org.traccar.storage.query.Request;
+import com.nimbusds.oauth2.sdk.ParseException;
+import javax.annotation.Nullable;
import javax.annotation.security.PermitAll;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
@@ -49,6 +52,7 @@ import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Date;
+import java.net.URI;
@Path("session")
@Produces(MediaType.APPLICATION_JSON)
@@ -63,6 +67,10 @@ public class SessionResource extends BaseResource {
private LoginService loginService;
@Inject
+ @Nullable
+ private OpenIdProvider openIdProvider;
+
+ @Inject
private TokenManager tokenManager;
@Context
@@ -160,4 +168,21 @@ public class SessionResource extends BaseResource {
return tokenManager.generateToken(getUserId(), expiration);
}
+ @PermitAll
+ @Path("openid/auth")
+ @GET
+ public Response openIdAuth() throws IOException {
+ return Response.seeOther(openIdProvider.createAuthUri()).build();
+ }
+
+ @PermitAll
+ @Path("openid/callback")
+ @GET
+ public Response requestToken() throws IOException, StorageException, ParseException, GeneralSecurityException {
+ StringBuilder requestUrl = new StringBuilder(request.getRequestURL().toString());
+ String queryString = request.getQueryString();
+ String requestUri = requestUrl.append('?').append(queryString).toString();
+
+ return Response.seeOther(openIdProvider.handleCallback(URI.create(requestUri), request)).build();
+ }
}
diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java
index e41ebbe61..19d88782f 100644
--- a/src/main/java/org/traccar/api/resource/UserResource.java
+++ b/src/main/java/org/traccar/api/resource/UserResource.java
@@ -17,7 +17,6 @@ package org.traccar.api.resource;
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;
@@ -39,7 +38,6 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Collection;
-import java.util.Date;
@Path("users")
@Produces(MediaType.APPLICATION_JSON)
@@ -91,11 +89,7 @@ public class UserResource extends BaseObjectResource<User> {
if (!permissionsService.getServer().getRegistration()) {
throw new SecurityException("Registration disabled");
}
- entity.setDeviceLimit(config.getInteger(Keys.USERS_DEFAULT_DEVICE_LIMIT));
- int expirationDays = config.getInteger(Keys.USERS_DEFAULT_EXPIRATION_DAYS);
- if (expirationDays > 0) {
- entity.setExpirationTime(new Date(System.currentTimeMillis() + expirationDays * 86400000L));
- }
+ UserUtil.setUserDefaults(entity, config);
}
}
diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java
index 88bafcfb5..c7482a2e3 100644
--- a/src/main/java/org/traccar/api/security/LoginService.java
+++ b/src/main/java/org/traccar/api/security/LoginService.java
@@ -19,6 +19,7 @@ import org.traccar.api.signature.TokenManager;
import org.traccar.config.Config;
import org.traccar.config.Keys;
import org.traccar.database.LdapProvider;
+import org.traccar.helper.model.UserUtil;
import org.traccar.model.User;
import org.traccar.storage.Storage;
import org.traccar.storage.StorageException;
@@ -35,6 +36,7 @@ import java.security.GeneralSecurityException;
@Singleton
public class LoginService {
+ private final Config config;
private final Storage storage;
private final TokenManager tokenManager;
private final LdapProvider ldapProvider;
@@ -46,6 +48,7 @@ public class LoginService {
public LoginService(
Config config, Storage storage, TokenManager tokenManager, @Nullable LdapProvider ldapProvider) {
this.storage = storage;
+ this.config = config;
this.tokenManager = tokenManager;
this.ldapProvider = ldapProvider;
serviceAccountToken = config.getString(Keys.WEB_SERVICE_ACCOUNT_TOKEN);
@@ -89,6 +92,27 @@ public class LoginService {
return null;
}
+ 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 {
+ user = new User();
+ UserUtil.setUserDefaults(user, config);
+ user.setName(name);
+ user.setEmail(email);
+ user.setFixedEmail(true);
+ user.setAdministrator(administrator);
+ user.setId(storage.addObject(user, new Request(new Columns.Exclude("id"))));
+ checkUserEnabled(user);
+ return user;
+ }
+ }
+
private void checkUserEnabled(User user) throws SecurityException {
if (user == null) {
throw new SecurityException("Unknown account");
diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java
index c207efb1e..707e9e815 100644
--- a/src/main/java/org/traccar/config/Keys.java
+++ b/src/main/java/org/traccar/config/Keys.java
@@ -611,6 +611,68 @@ public final class Keys {
List.of(KeyType.CONFIG));
/**
+ * Force OpenID Connect authentication. When enabled, the Traccar login page will be skipped
+ * and users are redirected to the OpenID Connect provider.
+ */
+ public static final ConfigKey<Boolean> OPENID_FORCE = new BooleanConfigKey(
+ "openid.force",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * OpenID Connect Client ID.
+ * This is a unique ID assigned to each application you register with your identity provider.
+ * Required to enable SSO.
+ */
+ public static final ConfigKey<String> OPENID_CLIENTID = new StringConfigKey(
+ "openid.clientId",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * OpenID Connect Client Secret.
+ * This is a secret assigned to each application you register with your identity provider.
+ * Required to enable SSO.
+ */
+ public static final ConfigKey<String> OPENID_CLIENTSECRET = new StringConfigKey(
+ "openid.clientSecret",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * OpenID Connect Authorization URL.
+ * This can usually be found in the documentation of your identity provider or by using the well-known
+ * configuration endpoint, eg. https://auth.example.com//.well-known/openid-configuration
+ * Required to enable SSO.
+ */
+ public static final ConfigKey<String> OPENID_AUTHURL = new StringConfigKey(
+ "openid.authUrl",
+ List.of(KeyType.CONFIG));
+ /**
+ * OpenID Connect Token URL.
+ * This can be found in the same ways at openid.authUrl.
+ * Required to enable SSO.
+ */
+ public static final ConfigKey<String> OPENID_TOKENURL = new StringConfigKey(
+ "openid.tokenUrl",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * OpenID Connect User Info URL.
+ * This can be found in the same ways at openid.authUrl.
+ * Required to enable SSO.
+ */
+ public static final ConfigKey<String> OPENID_USERINFOURL = new StringConfigKey(
+ "openid.userInfoUrl",
+ List.of(KeyType.CONFIG));
+
+ /**
+ * OpenID Connect group to grant admin access.
+ * Defaults to admins.
+ */
+ public static final ConfigKey<String> OPENID_ADMINGROUP = new StringConfigKey(
+ "openid.adminGroup",
+ List.of(KeyType.CONFIG),
+ "admins");
+
+ /**
* If no data is reported by a device for the given amount of time, status changes from online to unknown. Value is
* in seconds. Default timeout is 10 minutes.
*/
@@ -1573,7 +1635,7 @@ public final class Keys {
List.of(KeyType.CONFIG));
/**
- * Public URL for the web app. Used for notification and report link.
+ * Public URL for the web app. Used for notification, report link and OpenID Connect.
* If not provided, Traccar will attempt to get a URL from the server IP address, but it might be a local address.
*/
public static final ConfigKey<String> WEB_URL = new StringConfigKey(
diff --git a/src/main/java/org/traccar/database/OpenIdProvider.java b/src/main/java/org/traccar/database/OpenIdProvider.java
new file mode 100644
index 000000000..f5c7eef15
--- /dev/null
+++ b/src/main/java/org/traccar/database/OpenIdProvider.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2023 Daniel Raper (me@danr.uk)
+ *
+ * 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.database;
+
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.api.resource.SessionResource;
+import org.traccar.api.security.LoginService;
+import org.traccar.model.User;
+import org.traccar.storage.StorageException;
+import org.traccar.helper.LogAction;
+import org.traccar.helper.ServletHelper;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import com.google.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.AuthorizationResponse;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+
+public class OpenIdProvider {
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdProvider.class);
+
+ private final Boolean force;
+ private final ClientID clientId;
+ private final ClientAuthentication clientAuth;
+ private URI callbackUrl;
+ private URI authUrl;
+ private URI tokenUrl;
+ private URI userInfoUrl;
+ private URI baseUrl;
+ private final String adminGroup;
+
+ private LoginService loginService;
+
+ @Inject
+ public OpenIdProvider(Config config, LoginService loginService) {
+ this.loginService = loginService;
+
+ force = config.getBoolean(Keys.OPENID_FORCE);
+ clientId = new ClientID(config.getString(Keys.OPENID_CLIENTID));
+ clientAuth = new ClientSecretBasic(clientId, new Secret(config.getString(Keys.OPENID_CLIENTSECRET)));
+
+ try {
+ callbackUrl = new URI(config.getString(Keys.WEB_URL, "") + "/api/session/openid/callback");
+ authUrl = new URI(config.getString(Keys.OPENID_AUTHURL, ""));
+ tokenUrl = new URI(config.getString(Keys.OPENID_TOKENURL, ""));
+ userInfoUrl = new URI(config.getString(Keys.OPENID_USERINFOURL, ""));
+ baseUrl = new URI(config.getString(Keys.WEB_URL, ""));
+ } catch (URISyntaxException error) {
+ LOGGER.error("Invalid URIs provided in OpenID configuration");
+ }
+
+ adminGroup = config.getString(Keys.OPENID_ADMINGROUP);
+ }
+
+ public URI createAuthUri() {
+ AuthenticationRequest.Builder request = new AuthenticationRequest.Builder(
+ new ResponseType("code"),
+ new Scope("openid", "profile", "email", "groups"),
+ clientId,
+ callbackUrl);
+
+ return request.endpointURI(authUrl)
+ .state(new State())
+ .build()
+ .toURI();
+ }
+
+ private OIDCTokenResponse getToken(
+ AuthorizationCode code) throws IOException, ParseException, GeneralSecurityException {
+ AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callbackUrl);
+ TokenRequest tokenRequest = new TokenRequest(tokenUrl, clientAuth, codeGrant);
+
+ HTTPResponse tokenResponse = tokenRequest.toHTTPRequest().send();
+ TokenResponse token = OIDCTokenResponseParser.parse(tokenResponse);
+ if (!token.indicatesSuccess()) {
+ throw new GeneralSecurityException("Unable to authenticate with the OpenID Connect provider.");
+ }
+
+ return (OIDCTokenResponse) token.toSuccessResponse();
+ }
+
+ private UserInfo getUserInfo(BearerAccessToken token) throws IOException, ParseException, GeneralSecurityException {
+ HTTPResponse httpResponse = new UserInfoRequest(userInfoUrl, token)
+ .toHTTPRequest()
+ .send();
+
+ UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
+
+ if (!userInfoResponse.indicatesSuccess()) {
+ throw new GeneralSecurityException(
+ "Failed to access OpenID Connect user info endpoint. Please contact your administrator.");
+ }
+
+ return userInfoResponse.toSuccessResponse().getUserInfo();
+ }
+
+ public URI handleCallback(
+ URI requestUri, HttpServletRequest request
+ ) throws StorageException, ParseException, IOException, GeneralSecurityException {
+ AuthorizationResponse response = AuthorizationResponse.parse(requestUri);
+
+ if (!response.indicatesSuccess()) {
+ throw new GeneralSecurityException(response.toErrorResponse().getErrorObject().getDescription());
+ }
+
+ AuthorizationCode authCode = response.toSuccessResponse().getAuthorizationCode();
+
+ if (authCode == null) {
+ throw new GeneralSecurityException("Malformed OpenID callback.");
+ }
+
+ OIDCTokenResponse tokens = getToken(authCode);
+
+ BearerAccessToken bearerToken = tokens.getOIDCTokens().getBearerAccessToken();
+
+ UserInfo userInfo = getUserInfo(bearerToken);
+
+ User user = loginService.login(
+ userInfo.getEmailAddress(), userInfo.getName(),
+ userInfo.getStringListClaim("groups").contains(adminGroup));
+
+ request.getSession().setAttribute(SessionResource.USER_ID_KEY, user.getId());
+ LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request));
+
+ return baseUrl;
+ }
+
+ public boolean getForce() {
+ return force;
+ }
+}
diff --git a/src/main/java/org/traccar/helper/model/UserUtil.java b/src/main/java/org/traccar/helper/model/UserUtil.java
index 9f93afeae..4b1c404f9 100644
--- a/src/main/java/org/traccar/helper/model/UserUtil.java
+++ b/src/main/java/org/traccar/helper/model/UserUtil.java
@@ -15,6 +15,8 @@
*/
package org.traccar.helper.model;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
import org.traccar.model.Server;
import org.traccar.model.User;
import org.traccar.storage.Storage;
@@ -23,6 +25,7 @@ import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Order;
import org.traccar.storage.query.Request;
+import java.util.Date;
import java.util.TimeZone;
public final class UserUtil {
@@ -65,4 +68,11 @@ public final class UserUtil {
return preference != null ? preference : defaultValue;
}
+ public static void setUserDefaults(User user, Config config) {
+ user.setDeviceLimit(config.getInteger(Keys.USERS_DEFAULT_DEVICE_LIMIT));
+ int expirationDays = config.getInteger(Keys.USERS_DEFAULT_EXPIRATION_DAYS);
+ if (expirationDays > 0) {
+ user.setExpirationTime(new Date(System.currentTimeMillis() + expirationDays * 86400000L));
+ }
+ }
}
diff --git a/src/main/java/org/traccar/model/Server.java b/src/main/java/org/traccar/model/Server.java
index 73645721b..b790ca472 100644
--- a/src/main/java/org/traccar/model/Server.java
+++ b/src/main/java/org/traccar/model/Server.java
@@ -16,6 +16,7 @@
package org.traccar.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
import org.traccar.storage.QueryIgnore;
import org.traccar.storage.StorageName;
@@ -261,4 +262,27 @@ public class Server extends ExtendedModel implements UserRestrictions {
this.newServer = newServer;
}
+ private boolean openIdEnabled;
+
+ @QueryIgnore
+ public boolean getOpenIdEnabled() {
+ return openIdEnabled;
+ }
+
+ @QueryIgnore
+ public void setOpenIdEnabled(boolean openIdEnabled) {
+ this.openIdEnabled = openIdEnabled;
+ }
+
+ private boolean openIdForce;
+
+ @QueryIgnore
+ public boolean getOpenIdForce() {
+ return openIdForce;
+ }
+
+ @QueryIgnore
+ public void setOpenIdForce(boolean openIdForce) {
+ this.openIdForce = openIdForce;
+ }
}