From 92ebe0adba132bed5499bc1624620afeea34e347 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 1 Apr 2023 22:41:36 +0100 Subject: Initial implementation --- .../org/traccar/api/resource/SessionResource.java | 25 +++ .../org/traccar/api/security/OpenIDProvider.java | 224 +++++++++++++++++++-- 2 files changed, 236 insertions(+), 13 deletions(-) (limited to 'src/main/java/org/traccar/api') diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index ff84c135f..ca9f37667 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -17,6 +17,7 @@ package org.traccar.api.resource; import org.traccar.api.BaseResource; import org.traccar.api.security.LoginService; +import org.traccar.api.security.OpenIDProvider; import org.traccar.api.signature.TokenManager; import org.traccar.helper.DataConverter; import org.traccar.helper.LogAction; @@ -49,6 +50,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) @@ -62,6 +64,9 @@ public class SessionResource extends BaseResource { @Inject private LoginService loginService; + @Inject + private OpenIDProvider openIdProvider; + @Inject private TokenManager tokenManager; @@ -160,4 +165,24 @@ public class SessionResource extends BaseResource { return tokenManager.generateToken(getUserId(), expiration); } + @PermitAll + @Path("openid/auth") + @GET + public Response openIdAuth() throws IOException { + return Response.seeOther( + openIdProvider.createAuthRequest() + ).build(); + } + + @PermitAll + @Path("openid/callback") + @GET + public Response requestToken() throws IOException, StorageException { + // Get full request URI + StringBuilder requestURL = new StringBuilder(request.getRequestURL().toString()); + String queryString = request.getQueryString(); + String requestURI = requestURL.append('?').append(queryString).toString(); + + return openIdProvider.handleCallback(URI.create(requestURI), request); + } } diff --git a/src/main/java/org/traccar/api/security/OpenIDProvider.java b/src/main/java/org/traccar/api/security/OpenIDProvider.java index 4eaf9ac21..cd3fa4dde 100644 --- a/src/main/java/org/traccar/api/security/OpenIDProvider.java +++ b/src/main/java/org/traccar/api/security/OpenIDProvider.java @@ -15,30 +15,228 @@ */ package org.traccar.api.security; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.traccar.config.Config; import org.traccar.config.Keys; +import org.traccar.api.resource.SessionResource; import org.traccar.model.User; +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Request; +import org.traccar.storage.query.Columns; +import org.traccar.helper.LogAction; +import org.traccar.helper.ServletHelper; -public class OpenIDProvider { +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Date; +import java.util.List; +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import com.google.inject.Inject; + +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.Nonce; +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; - private static final Logger LOGGER = LoggerFactory.getLogger(OpenIDProvider.class); +import com.nimbusds.openid.connect.sdk.claims.UserInfo; +public class OpenIDProvider { private final Boolean force; - private final String clientId; - private final String authUrl; - private final String tokenUrl; - private final String userInfoUrl; + private final ClientID clientId; + private final Secret clientSecret; + private URI callbackUrl; + private URI authUrl; + private URI tokenUrl; + private URI userInfoUrl; + private URI baseUrl; private final String adminGroup; - public OpenIDProvider(Config config) { + private Config config; + private LoginService loginService; + private Storage storage; + + @Inject + public OpenIDProvider(Config config, LoginService loginService, Storage storage) { force = config.getBoolean(Keys.OIDC_FORCE); - clientId = config.getString(Keys.OIDC_CLIENTID); - authUrl = config.getString(Keys.OIDC_AUTHURL); - tokenUrl = config.getString(Keys.OIDC_TOKENURL); - userInfoUrl = config.getString(Keys.OIDC_USERINFOURL); + clientId = new ClientID(config.getString(Keys.OIDC_CLIENTID)); + clientSecret = new Secret(config.getString(Keys.OIDC_CLIENTSECRET)); + + this.config = config; + this.storage = storage; + this.loginService = loginService; + + try { + callbackUrl = new URI(config.getString(Keys.WEB_URL, "") + "/api/session/openid/callback"); + authUrl = new URI(config.getString(Keys.OIDC_AUTHURL, "")); + tokenUrl = new URI(config.getString(Keys.OIDC_TOKENURL, "")); + userInfoUrl = new URI(config.getString(Keys.OIDC_USERINFOURL, "")); + baseUrl = new URI(config.getString(Keys.WEB_URL, "")); + } catch (URISyntaxException e) { + } + adminGroup = config.getString(Keys.OIDC_ADMINGROUP); } + public URI createAuthRequest() { + AuthenticationRequest request = new AuthenticationRequest.Builder( + new ResponseType("code"), + new Scope("openid", "profile", "email", "groups"), + this.clientId, + this.callbackUrl) + .endpointURI(this.authUrl) + .state(new State()) + .nonce(new Nonce()) + .build(); + + return request.toURI(); + } + + private OIDCTokenResponse getToken(AuthorizationCode code) { + // Credentials to authenticate us to the token endpoint + ClientAuthentication clientAuth = new ClientSecretBasic(this.clientId, this.clientSecret); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, this.callbackUrl); + + TokenRequest request = new TokenRequest(this.tokenUrl, clientAuth, codeGrant); + TokenResponse tokenResponse; + + try { + HTTPResponse tokenReq = request.toHTTPRequest().send(); + tokenResponse = OIDCTokenResponseParser.parse(tokenReq); + if (!tokenResponse.indicatesSuccess()) { + return null; + } + + return (OIDCTokenResponse) tokenResponse.toSuccessResponse(); + } catch (IOException e) { + return null; + } catch (ParseException e) { + return null; + } + } + + private AuthorizationCode parseCallback(URI requri) { + AuthorizationResponse response; + + try { + response = AuthorizationResponse.parse(requri); + } catch (ParseException e) { + return null; + } + + if (!response.indicatesSuccess()) { + return null; + } + + return response.toSuccessResponse().getAuthorizationCode(); + } + + private UserInfo getUserInfo(BearerAccessToken token) { + UserInfoResponse userInfoResponse; + + try { + HTTPResponse httpResponse = new UserInfoRequest(this.userInfoUrl, token) + .toHTTPRequest() + .send(); + + userInfoResponse = UserInfoResponse.parse(httpResponse); + } catch (IOException e) { + return null; + } catch (ParseException e) { + return null; + } + + if (!userInfoResponse.indicatesSuccess()) { + // User info request failed - usually from expiring + return null; + } + + return userInfoResponse.toSuccessResponse().getUserInfo(); + } + + private User createUser(String name, String email, Boolean administrator) throws StorageException { + User user = new User(); + + user.setName(name); + user.setEmail(email); + user.setFixedEmail(true); + user.setDeviceLimit(this.config.getInteger(Keys.USERS_DEFAULT_DEVICE_LIMIT)); + + int expirationDays = this.config.getInteger(Keys.USERS_DEFAULT_EXPIRATION_DAYS); + + if (expirationDays > 0) { + user.setExpirationTime(new Date(System.currentTimeMillis() + expirationDays * 86400000L)); + } + + if (administrator) { + user.setAdministrator(true); + } + + user.setId(this.storage.addObject(user, new Request(new Columns.Exclude("id")))); + + return user; + } + + public Response handleCallback(URI requri, HttpServletRequest request) throws StorageException { + // Parse callback + AuthorizationCode authCode = this.parseCallback(requri); + + if (authCode == null) { + return Response.ok().entity("Callback parse fail").build(); + } + + // Get token from IDP + OIDCTokenResponse tokens = this.getToken(authCode); + + if (tokens == null) { + return Response.ok().entity("Token request failed").build(); + } + + BearerAccessToken bearerToken = tokens.getOIDCTokens().getBearerAccessToken(); + + // Get user info from IDP + UserInfo idpUser = this.getUserInfo(bearerToken); + + if (idpUser == null) { + return Response.ok().entity("User info request failed").build(); + } + + String email = idpUser.getEmailAddress(); + String name = idpUser.getName(); + + // Check if user exists + User user = this.loginService.lookup(email); + + // If user does not exist, create one + if (user == null) { + List groups = idpUser.getStringListClaim("groups"); + Boolean administrator = groups.contains(this.adminGroup); + user = this.createUser(name, email, administrator); + } + + // Set user session and redirect to homepage + request.getSession().setAttribute(SessionResource.USER_ID_KEY, user.getId()); + LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); + return Response.seeOther( + baseUrl).build(); + } } -- cgit v1.2.3