This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch 3074-support-third-party-sign-in
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to
refs/heads/3074-support-third-party-sign-in by this push:
new 8769b1458a feat(#3074): Support OAuth login
8769b1458a is described below
commit 8769b1458ac7eb1849365ef0355e5da95f2f40b3
Author: Dominik Riemer <[email protected]>
AuthorDate: Tue Jul 30 10:19:24 2024 +0200
feat(#3074): Support OAuth login
---
.../apache/streampipes/commons/constants/Envs.java | 2 +
.../commons/environment/DefaultEnvironment.java | 19 +++
.../commons/environment/Environment.java | 9 ++
.../environment/model/OAuthConfiguration.java | 150 ++++++++++++++++++++
.../parser/OAuthConfigurationParser.java | 95 +++++++++++++
.../parser/OAuthConfigurationParserTest.java | 69 ++++++++++
streampipes-model-client/pom.xml | 3 +
.../streampipes/model/client/user/UserAccount.java | 13 ++
.../model/client/user/UserRegistrationData.java | 35 ++++-
.../resource/management/UserResourceManager.java | 17 ++-
.../rest/shared/exception/BadRequestException.java | 73 +++++-----
.../streampipes/rest/impl/Authentication.java | 25 +++-
.../apache/streampipes/rest/impl/UserResource.java | 6 +
.../OAuth2AuthenticationProcessingException.java | 71 +++++-----
.../minimal/StreamPipesCoreApplicationMinimal.java | 5 +-
streampipes-service-core/pom.xml | 5 +
.../service/core/StreamPipesCoreApplication.java | 8 +-
.../service/core/WebSecurityConfig.java | 151 +++++++++++++++++++--
.../core/oauth2/CustomOAuth2UserService.java | 48 +++++++
.../service/core/oauth2/CustomOidcUserService.java | 51 +++++++
...CookieOAuth2AuthorizationRequestRepository.java | 84 ++++++++++++
...h2AccessTokenResponseConverterWithDefaults.java | 89 ++++++++++++
.../oauth2/OAuth2AuthenticationFailureHandler.java | 63 +++++++++
.../oauth2/OAuth2AuthenticationSuccessHandler.java | 108 +++++++++++++++
.../core/oauth2/OidcUserAccountDetails.java | 82 +++++++++++
.../service/core/oauth2/UserService.java | 104 ++++++++++++++
.../service/core/oauth2/util/CookieUtils.java | 81 +++++++++++
.../src/lib/model/gen/streampipes-model-client.ts | 15 +-
.../src/lib/model/gen/streampipes-model.ts | 2 +-
.../warning-box/warning-box.component.html | 21 +++
.../warning-box/warning-box.component.scss | 13 +-
.../warning-box/warning-box.component.ts | 13 +-
.../shared-ui/src/lib/shared-ui.module.ts | 3 +
.../streampipes/shared-ui/src/public-api.ts | 1 +
.../edit-user-dialog.component.html | 5 +-
.../edit-user-dialog/edit-user-dialog.component.ts | 30 ++--
.../security-user-config.component.html | 13 ++
.../security-user-config.component.ts | 6 +-
.../login/components/login/login.component.html | 33 ++++-
.../login/components/login/login.component.scss | 13 ++
.../app/login/components/login/login.component.ts | 12 +-
ui/src/app/login/components/login/login.model.ts | 12 ++
.../general-profile-settings.component.html | 12 +-
.../general/general-profile-settings.component.ts | 2 +
ui/src/app/services/auth.service.ts | 9 ++
45 files changed, 1546 insertions(+), 135 deletions(-)
diff --git
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/Envs.java
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/Envs.java
index 672014852e..00765b76b5 100644
---
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/Envs.java
+++
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/Envs.java
@@ -42,6 +42,8 @@ public enum Envs {
SP_CLIENT_USER("SP_CLIENT_USER",
DefaultEnvValues.INITIAL_CLIENT_USER_DEFAULT),
SP_CLIENT_SECRET("SP_CLIENT_SECRET",
DefaultEnvValues.INITIAL_CLIENT_SECRET_DEFAULT),
SP_ENCRYPTION_PASSCODE("SP_ENCRYPTION_PASSCODE",
DefaultEnvValues.DEFAULT_ENCRYPTION_PASSCODE),
+ SP_OAUTH_ENABLED("SP_OAUTH_ENABLED", "false"),
+ SP_OAUTH_REDIRECT_URI("SP_OAUTH_REDIRECT_URI"),
SP_DEBUG("SP_DEBUG", "false"),
SP_MAX_WAIT_TIME_AT_SHUTDOWN("SP_MAX_WAIT_TIME_AT_SHUTDOWN"),
diff --git
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/DefaultEnvironment.java
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/DefaultEnvironment.java
index 463a3a1d3e..5b6d5428d6 100644
---
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/DefaultEnvironment.java
+++
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/DefaultEnvironment.java
@@ -19,10 +19,14 @@
package org.apache.streampipes.commons.environment;
import org.apache.streampipes.commons.constants.Envs;
+import org.apache.streampipes.commons.environment.model.OAuthConfiguration;
+import
org.apache.streampipes.commons.environment.parser.OAuthConfigurationParser;
import
org.apache.streampipes.commons.environment.variable.BooleanEnvironmentVariable;
import
org.apache.streampipes.commons.environment.variable.IntEnvironmentVariable;
import
org.apache.streampipes.commons.environment.variable.StringEnvironmentVariable;
+import java.util.List;
+
public class DefaultEnvironment implements Environment {
@Override
@@ -174,6 +178,21 @@ public class DefaultEnvironment implements Environment {
return new StringEnvironmentVariable(Envs.SP_ENCRYPTION_PASSCODE);
}
+ @Override
+ public BooleanEnvironmentVariable getOAuthEnabled() {
+ return new BooleanEnvironmentVariable(Envs.SP_OAUTH_ENABLED);
+ }
+
+ @Override
+ public StringEnvironmentVariable getOAuthRedirectUri() {
+ return new StringEnvironmentVariable(Envs.SP_OAUTH_REDIRECT_URI);
+ }
+
+ @Override
+ public List<OAuthConfiguration> getOAuthConfigurations() {
+ return new OAuthConfigurationParser().parse(System.getenv());
+ }
+
@Override
public StringEnvironmentVariable getKafkaRetentionTimeMs() {
return new StringEnvironmentVariable(Envs.SP_KAFKA_RETENTION_MS);
diff --git
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/Environment.java
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/Environment.java
index aabae364b4..1a72910522 100644
---
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/Environment.java
+++
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/Environment.java
@@ -18,10 +18,13 @@
package org.apache.streampipes.commons.environment;
+import org.apache.streampipes.commons.environment.model.OAuthConfiguration;
import
org.apache.streampipes.commons.environment.variable.BooleanEnvironmentVariable;
import
org.apache.streampipes.commons.environment.variable.IntEnvironmentVariable;
import
org.apache.streampipes.commons.environment.variable.StringEnvironmentVariable;
+import java.util.List;
+
public interface Environment {
BooleanEnvironmentVariable getSpDebug();
@@ -91,6 +94,12 @@ public interface Environment {
StringEnvironmentVariable getEncryptionPasscode();
+ BooleanEnvironmentVariable getOAuthEnabled();
+
+ StringEnvironmentVariable getOAuthRedirectUri();
+
+ List<OAuthConfiguration> getOAuthConfigurations();
+
// Messaging
StringEnvironmentVariable getKafkaRetentionTimeMs();
diff --git
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
new file mode 100644
index 0000000000..7ab566f460
--- /dev/null
+++
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.commons.environment.model;
+
+public class OAuthConfiguration {
+
+ private String authorizationUri;
+ private String clientName;
+ private String clientId;
+ private String clientSecret;
+ private String fullNameAttributeName;
+ private String issuerUri;
+ private String jwkSetUri;
+ private String registrationId;
+ private String registrationName;
+ private String[] scopes;
+ private String tokenUri;
+ private String userInfoUri;
+ private String emailAttributeName;
+ private String userIdAttributeName;
+
+
+ public String getRegistrationId() {
+ return registrationId;
+ }
+
+ public void setRegistrationId(String registrationId) {
+ this.registrationId = registrationId;
+ }
+
+ public String[] getScopes() {
+ return scopes;
+ }
+
+ public void setScopes(String[] scopes) {
+ this.scopes = scopes;
+ }
+
+ public String getAuthorizationUri() {
+ return authorizationUri;
+ }
+
+ public void setAuthorizationUri(String authorizationUri) {
+ this.authorizationUri = authorizationUri;
+ }
+
+ public String getTokenUri() {
+ return tokenUri;
+ }
+
+ public void setTokenUri(String tokenUri) {
+ this.tokenUri = tokenUri;
+ }
+
+ public String getJwkSetUri() {
+ return jwkSetUri;
+ }
+
+ public void setJwkSetUri(String jwkSetUri) {
+ this.jwkSetUri = jwkSetUri;
+ }
+
+ public String getIssuerUri() {
+ return issuerUri;
+ }
+
+ public void setIssuerUri(String issuerUri) {
+ this.issuerUri = issuerUri;
+ }
+
+ public String getUserInfoUri() {
+ return userInfoUri;
+ }
+
+ public void setUserInfoUri(String userInfoUri) {
+ this.userInfoUri = userInfoUri;
+ }
+
+ public String getClientName() {
+ return clientName;
+ }
+
+ public void setClientName(String clientName) {
+ this.clientName = clientName;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public String getEmailAttributeName() {
+ return emailAttributeName;
+ }
+
+ public void setEmailAttributeName(String emailAttributeName) {
+ this.emailAttributeName = emailAttributeName;
+ }
+
+ public String getFullNameAttributeName() {
+ return fullNameAttributeName;
+ }
+
+ public void setFullNameAttributeName(String fullNameAttributeName) {
+ this.fullNameAttributeName = fullNameAttributeName;
+ }
+
+ public String getUserIdAttributeName() {
+ return userIdAttributeName;
+ }
+
+ public void setUserIdAttributeName(String userIdAttributeName) {
+ this.userIdAttributeName = userIdAttributeName;
+ }
+
+ public String getRegistrationName() {
+ return registrationName;
+ }
+
+ public void setRegistrationName(String registrationName) {
+ this.registrationName = registrationName;
+ }
+}
diff --git
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
new file mode 100644
index 0000000000..520bc38150
--- /dev/null
+++
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.commons.environment.parser;
+
+import org.apache.streampipes.commons.environment.model.OAuthConfiguration;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OAuthConfigurationParser {
+
+ private static final String OAUTH_PREFIX = "SP_OAUTH_PROVIDER";
+
+ public List<OAuthConfiguration> parse(Map<String, String> env) {
+ Map<String, OAuthConfiguration> oAuthConfigurationsMap = new HashMap<>();
+
+ for (Map.Entry<String, String> entry : env.entrySet()) {
+ String key = entry.getKey();
+ if (key.startsWith(OAUTH_PREFIX)) {
+ String[] parts = key.split("_");
+ if (parts.length >= 5) {
+ String registrationId = parts[3].toLowerCase();
+ String settingName = String.join("_", Arrays.copyOfRange(parts, 4,
parts.length));
+
+ OAuthConfiguration config =
oAuthConfigurationsMap.getOrDefault(registrationId, new OAuthConfiguration());
+ config.setRegistrationId(registrationId);
+
+ switch (settingName) {
+ case "AUTHORIZATION_URI":
+ config.setAuthorizationUri(entry.getValue());
+ break;
+ case "CLIENT_NAME":
+ config.setClientName(entry.getValue());
+ break;
+ case "CLIENT_ID":
+ config.setClientId(entry.getValue());
+ break;
+ case "CLIENT_SECRET":
+ config.setClientSecret(entry.getValue());
+ break;
+ case "FULL_NAME_ATTRIBUTE_NAME":
+ config.setFullNameAttributeName(entry.getValue());
+ break;
+ case "ISSUER_URI":
+ config.setIssuerUri(entry.getValue());
+ break;
+ case "JWK_SET_URI":
+ config.setJwkSetUri(entry.getValue());
+ break;
+ case "SCOPES":
+ config.setScopes(entry.getValue().split(","));
+ break;
+ case "TOKEN_URI":
+ config.setTokenUri(entry.getValue());
+ break;
+ case "USER_INFO_URI":
+ config.setUserInfoUri(entry.getValue());
+ break;
+ case "EMAIL_ATTRIBUTE_NAME":
+ config.setEmailAttributeName(entry.getValue());
+ break;
+ case "USER_ID_ATTRIBUTE_NAME":
+ config.setUserIdAttributeName(entry.getValue());
+ break;
+ case "NAME":
+ config.setRegistrationName(entry.getValue());
+ }
+
+ oAuthConfigurationsMap.put(registrationId, config);
+ }
+ }
+ }
+
+ return new ArrayList<>(oAuthConfigurationsMap.values());
+ }
+}
diff --git
a/streampipes-commons/src/test/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParserTest.java
b/streampipes-commons/src/test/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParserTest.java
new file mode 100644
index 0000000000..feb40aca03
--- /dev/null
+++
b/streampipes-commons/src/test/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParserTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.commons.environment.parser;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class OAuthConfigurationParserTest {
+
+ private final Map<String, String> env = new HashMap<>() {{
+ put("SP_OAUTH_PROVIDER_A_AUTHORIZATION_URI", "authorizationUriA");
+ put("SP_OAUTH_PROVIDER_A_CLIENT_NAME", "clientNameA");
+ put("SP_OAUTH_PROVIDER_A_CLIENT_ID", "clientIdA");
+ put("SP_OAUTH_PROVIDER_A_CLIENT_SECRET", "clientSecretA");
+ put("SP_OAUTH_PROVIDER_A_FULL_NAME_ATTRIBUTE_NAME", "fullNameA");
+ put("SP_OAUTH_PROVIDER_A_ISSUER_URI", "issuerUriA");
+ put("SP_OAUTH_PROVIDER_A_JWK_SET_URI", "jwkSetUriA");
+ put("SP_OAUTH_PROVIDER_A_SCOPES", "scope1,scope2");
+ put("SP_OAUTH_PROVIDER_A_TOKEN_URI", "tokenUriA");
+ put("SP_OAUTH_PROVIDER_A_USER_INFO_URI", "userInfoUriA");
+ put("SP_OAUTH_PROVIDER_A_USER_ID_ATTRIBUTE_NAME", "userNameA");
+ put("SP_OAUTH_PROVIDER_BA_AUTHORIZATION_URI", "authorizationUriB");
+ }};
+
+ @Test
+ public void testParser() {
+ var config = new OAuthConfigurationParser().parse(env);
+
+ assertEquals(2, config.size());
+ assertEquals("a", config.get(0).getRegistrationId());
+ assertEquals("ba", config.get(1).getRegistrationId());
+ assertEquals("authorizationUriA", config.get(0).getAuthorizationUri());
+ assertEquals("authorizationUriB", config.get(1).getAuthorizationUri());
+ assertEquals("clientNameA", config.get(0).getClientName());
+ assertEquals("clientIdA", config.get(0).getClientId());
+ assertEquals("clientSecretA", config.get(0).getClientSecret());
+ assertEquals("fullNameA", config.get(0).getFullNameAttributeName());
+ assertEquals("issuerUriA", config.get(0).getIssuerUri());
+ assertEquals("jwkSetUriA", config.get(0).getJwkSetUri());
+ assertEquals(2, config.get(0).getScopes().length);
+ assertEquals("scope1", config.get(0).getScopes()[0]);
+ assertEquals("scope2", config.get(0).getScopes()[1]);
+ assertEquals("tokenUriA", config.get(0).getTokenUri());
+ assertEquals("userInfoUriA", config.get(0).getUserInfoUri());
+ assertEquals("userNameA", config.get(0).getUserIdAttributeName());
+ assertNull(config.get(1).getTokenUri());
+ }
+}
diff --git a/streampipes-model-client/pom.xml b/streampipes-model-client/pom.xml
index 2c2538ae54..7674d6dc42 100644
--- a/streampipes-model-client/pom.xml
+++ b/streampipes-model-client/pom.xml
@@ -88,6 +88,9 @@
<mapClasses>asClasses</mapClasses>
<sortDeclarations>true</sortDeclarations>
<tsNoCheck>true</tsNoCheck>
+ <importDeclarations>
+ <import>import { Storable } from
'@streampipes/platform-services'</import>
+ </importDeclarations>
<extensions>
<extension>cz.habarta.typescript.generator.ext.JsonDeserializationExtension</extension>
</extensions>
diff --git
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
index 3c6f5d96eb..4e80b32c63 100644
---
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
+++
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
@@ -27,6 +27,8 @@ import java.util.Set;
@TsModel
public class UserAccount extends Principal {
+ public static final String LOCAL = "local";
+
protected String fullName;
protected String password;
@@ -39,6 +41,8 @@ public class UserAccount extends Principal {
protected boolean hideTutorial;
protected boolean darkMode = false;
+ protected String provider;
+
public UserAccount() {
super(PrincipalType.USER_ACCOUNT);
this.hideTutorial = false;
@@ -46,6 +50,7 @@ public class UserAccount extends Principal {
this.preferredDataProcessors = new ArrayList<>();
this.preferredDataSinks = new ArrayList<>();
this.preferredDataStreams = new ArrayList<>();
+ this.provider = UserAccount.LOCAL;
}
public static UserAccount from(String username,
@@ -156,4 +161,12 @@ public class UserAccount extends Principal {
public void setPassword(String password) {
this.password = password;
}
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public void setProvider(String provider) {
+ this.provider = provider;
+ }
}
diff --git
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
index 5457645e38..92b1a0edd7 100644
---
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
+++
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
@@ -20,5 +20,38 @@ package org.apache.streampipes.model.client.user;
import java.util.List;
-public record UserRegistrationData(String username, String password,
List<String> roles) {
+public class UserRegistrationData {
+
+ private String username;
+ private String password;
+ private List<String> roles;
+ private String provider;
+
+ public UserRegistrationData(String username,
+ String password,
+ List<String> roles) {
+ this.username = username;
+ this.password = password;
+ this.roles = roles;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public List<String> getRoles() {
+ return roles;
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public void setProvider(String provider) {
+ this.provider = provider;
+ }
}
diff --git
a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/UserResourceManager.java
b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/UserResourceManager.java
index e9e1630be9..9fd8273ffa 100644
---
a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/UserResourceManager.java
+++
b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/UserResourceManager.java
@@ -90,19 +90,19 @@ public class UserResourceManager extends
AbstractResourceManager<IUserStorage> {
public void registerUser(UserRegistrationData data) throws
UsernameAlreadyTakenException {
try {
validateAndRegisterNewUser(data);
- createTokenAndSendActivationMail(data.username());
+ createTokenAndSendActivationMail(data.getUsername());
} catch (IOException e) {
LOG.error("Registration of user could not be completed: {}",
e.getMessage());
}
}
private synchronized void validateAndRegisterNewUser(UserRegistrationData
data) {
- if (db.checkUserExists(data.username())) {
+ if (db.checkUserExists(data.getUsername())) {
throw new UsernameAlreadyTakenException("Username already taken");
}
String encryptedPassword;
try {
- encryptedPassword = PasswordUtil.encryptPassword(data.password());
+ encryptedPassword = PasswordUtil.encryptPassword(data.getPassword());
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new SpException("Error during password encryption:
%s".formatted(e.getMessage()));
}
@@ -112,9 +112,9 @@ public class UserResourceManager extends
AbstractResourceManager<IUserStorage> {
private synchronized void createNewUser(UserRegistrationData data, String
encryptedPassword) {
- List<Role> roles = data.roles().stream().map(Role::valueOf).toList();
- UserAccount user = UserAccount.from(data.username(), encryptedPassword,
new HashSet<>(roles));
- user.setUsername(data.username());
+ List<Role> roles = data.getRoles().stream().map(Role::valueOf).toList();
+ UserAccount user = UserAccount.from(data.getUsername(), encryptedPassword,
new HashSet<>(roles));
+ user.setUsername(data.getUsername());
user.setPassword(encryptedPassword);
user.setAccountEnabled(false);
db.storeUser(user);
@@ -169,7 +169,7 @@ public class UserResourceManager extends
AbstractResourceManager<IUserStorage> {
PasswordRecoveryToken token =
getPasswordRecoveryTokenStorage().getElementById(recoveryCode);
Principal user = db.getUser(token.getUsername());
if (user instanceof UserAccount) {
- String encryptedPassword = PasswordUtil.encryptPassword(data.password());
+ String encryptedPassword =
PasswordUtil.encryptPassword(data.getPassword());
((UserAccount) user).setPassword(encryptedPassword);
db.updateUser(user);
getPasswordRecoveryTokenStorage().deleteElement(token);
@@ -194,4 +194,7 @@ public class UserResourceManager extends
AbstractResourceManager<IUserStorage> {
}
+ public void registerOauthUser(UserAccount userAccount) {
+ db.storeUser(userAccount);
+ }
}
diff --git a/ui/src/app/login/components/login/login.component.scss
b/streampipes-rest-shared/src/main/java/org/apache/streampipes/rest/shared/exception/BadRequestException.java
old mode 100644
new mode 100755
similarity index 63%
copy from ui/src/app/login/components/login/login.component.scss
copy to
streampipes-rest-shared/src/main/java/org/apache/streampipes/rest/shared/exception/BadRequestException.java
index 3b4edba73f..13e811d854
--- a/ui/src/app/login/components/login/login.component.scss
+++
b/streampipes-rest-shared/src/main/java/org/apache/streampipes/rest/shared/exception/BadRequestException.java
@@ -1,39 +1,34 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.
- *
- */
-
-.login-error {
- background: #ffa2a2;
- padding: 8px;
- color: #3e3e3e;
- border-radius: 5px;
-}
-
-.info-box {
- padding: 8px;
- border-radius: 5px;
-}
-
-.register-error {
- background: #ffa2a2;
- color: #3e3e3e;
-}
-
-.register-success {
- background: #a2ffa2;
- color: #3e3e3e;
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.rest.shared.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.BAD_REQUEST)
+public class BadRequestException extends RuntimeException {
+
+ public BadRequestException(String message) {
+ super(message);
+ }
+
+ public BadRequestException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
index 2401ee61da..3b3d2215ff 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/Authentication.java
@@ -18,6 +18,7 @@
package org.apache.streampipes.rest.impl;
+import org.apache.streampipes.commons.environment.Environments;
import org.apache.streampipes.commons.exceptions.UserNotFoundException;
import org.apache.streampipes.commons.exceptions.UsernameAlreadyTakenException;
import org.apache.streampipes.model.client.user.JwtAuthenticationResponse;
@@ -50,6 +51,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
@RestController
@@ -98,8 +100,8 @@ public class Authentication extends AbstractRestResource {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
var enrichedUserRegistrationData = new UserRegistrationData(
- userRegistrationData.username(),
- userRegistrationData.password(),
+ userRegistrationData.getUsername(),
+ userRegistrationData.getPassword(),
config.getDefaultUserRoles()
);
try {
@@ -139,6 +141,7 @@ public class Authentication extends AbstractRestResource {
response.put("allowSelfRegistration", config.isAllowSelfRegistration());
response.put("allowPasswordRecovery", config.isAllowPasswordRecovery());
response.put("linkSettings", config.getLinkSettings());
+ response.put("oAuthSettings", makeOAuthSettings());
return ok(response);
}
@@ -157,4 +160,22 @@ public class Authentication extends AbstractRestResource {
String jwt = new JwtTokenProvider().createToken(auth);
return new JwtAuthenticationResponse(jwt);
}
+
+ private UiOAuthSettings makeOAuthSettings() {
+ var env = Environments.getEnvironment();
+ var oAuthConfigs = env.getOAuthConfigurations();
+ return new UiOAuthSettings(
+ env.getOAuthEnabled().getValueOrDefault(),
+ env.getOAuthRedirectUri().getValueOrDefault(),
+ oAuthConfigs.stream().map(c -> new
OAuthProvider(c.getRegistrationName(), c.getRegistrationId())).toList()
+ );
+ }
+
+ private record UiOAuthSettings(boolean enabled,
+ String redirectUri,
+ List<OAuthProvider> supportedProviders) {
+ }
+
+ private record OAuthProvider(String name, String registrationId) {
+ }
}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
index ca66f10cc7..f27bc93144 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
@@ -294,6 +294,12 @@ public class UserResource extends
AbstractAuthGuardedRestResource {
boolean adminPrivileges,
String property) {
user.setPassword(property);
+ user.setProvider(existingUser.getProvider());
+ if (!existingUser.getProvider().equals(UserAccount.LOCAL)) {
+ // These settings are managed externally
+ user.setUsername(existingUser.getUsername());
+ user.setFullName(existingUser.getFullName());
+ }
if (!adminPrivileges) {
replacePermissions(user, existingUser);
}
diff --git a/ui/src/app/login/components/login/login.component.scss
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/OAuth2AuthenticationProcessingException.java
old mode 100644
new mode 100755
similarity index 66%
copy from ui/src/app/login/components/login/login.component.scss
copy to
streampipes-rest/src/main/java/org/apache/streampipes/rest/security/OAuth2AuthenticationProcessingException.java
index 3b4edba73f..308cf90980
--- a/ui/src/app/login/components/login/login.component.scss
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/OAuth2AuthenticationProcessingException.java
@@ -1,39 +1,32 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.
- *
- */
-
-.login-error {
- background: #ffa2a2;
- padding: 8px;
- color: #3e3e3e;
- border-radius: 5px;
-}
-
-.info-box {
- padding: 8px;
- border-radius: 5px;
-}
-
-.register-error {
- background: #ffa2a2;
- color: #3e3e3e;
-}
-
-.register-success {
- background: #a2ffa2;
- color: #3e3e3e;
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.rest.security;
+
+import org.springframework.security.core.AuthenticationException;
+
+public class OAuth2AuthenticationProcessingException extends
AuthenticationException {
+
+ public OAuth2AuthenticationProcessingException(String msg, Throwable t) {
+ super(msg, t);
+ }
+
+ public OAuth2AuthenticationProcessingException(String msg) {
+ super(msg);
+ }
+}
diff --git
a/streampipes-service-core-minimal/src/main/java/org/apache/streampipes/service/core/minimal/StreamPipesCoreApplicationMinimal.java
b/streampipes-service-core-minimal/src/main/java/org/apache/streampipes/service/core/minimal/StreamPipesCoreApplicationMinimal.java
index 5272d82d45..6db1b64dec 100644
---
a/streampipes-service-core-minimal/src/main/java/org/apache/streampipes/service/core/minimal/StreamPipesCoreApplicationMinimal.java
+++
b/streampipes-service-core-minimal/src/main/java/org/apache/streampipes/service/core/minimal/StreamPipesCoreApplicationMinimal.java
@@ -45,7 +45,10 @@ import java.util.List;
WebSecurityConfig.class,
WelcomePageController.class
})
-@ComponentScan({"org.apache.streampipes.rest.*", "org.apache.streampipes.ps"})
+@ComponentScan({
+ "org.apache.streampipes.rest.*",
+ "org.apache.streampipes.ps",
+ "org.apache.streampipes.service.core.oauth2"})
public class StreamPipesCoreApplicationMinimal extends
StreamPipesCoreApplication {
public static void main(String[] args) {
diff --git a/streampipes-service-core/pom.xml b/streampipes-service-core/pom.xml
index 2d7ecf7b1f..9d34826cc2 100644
--- a/streampipes-service-core/pom.xml
+++ b/streampipes-service-core/pom.xml
@@ -37,6 +37,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-oauth2-client</artifactId>
+ <version>${spring-boot.version}</version>
+ </dependency>
<!-- StreamPipes dependencies -->
<dependency>
<groupId>org.apache.streampipes</groupId>
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
index 588b194dfb..f9c062b546 100644
---
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
@@ -74,7 +74,11 @@ import java.util.concurrent.TimeUnit;
WebSecurityConfig.class,
WelcomePageController.class
})
-@ComponentScan({"org.apache.streampipes.rest.*", "org.apache.streampipes.ps"})
+@ComponentScan({
+ "org.apache.streampipes.rest.*",
+ "org.apache.streampipes.ps",
+ "org.apache.streampipes.service.core.oauth2"
+})
public class StreamPipesCoreApplication extends StreamPipesServiceBase {
private static final Logger LOG =
LoggerFactory.getLogger(StreamPipesCoreApplication.class.getCanonicalName());
@@ -147,7 +151,7 @@ public class StreamPipesCoreApplication extends
StreamPipesServiceBase {
StorageDispatcher.INSTANCE.getNoSqlStore().getAdapterInstanceStorage(),
new AdapterMasterManagement(
StorageDispatcher.INSTANCE.getNoSqlStore()
- .getAdapterInstanceStorage(),
+ .getAdapterInstanceStorage(),
new SpResourceManager().manageAdapters(),
new SpResourceManager().manageDataStreams(),
AdapterMetricsManager.INSTANCE.getAdapterMetrics()
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/WebSecurityConfig.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/WebSecurityConfig.java
index dc193709d1..141a68db12 100644
---
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/WebSecurityConfig.java
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/WebSecurityConfig.java
@@ -18,13 +18,25 @@
package org.apache.streampipes.service.core;
+import org.apache.streampipes.commons.environment.Environment;
+import org.apache.streampipes.commons.environment.Environments;
import
org.apache.streampipes.service.base.security.UnauthorizedRequestEntryPoint;
import org.apache.streampipes.service.core.filter.TokenAuthenticationFilter;
+import org.apache.streampipes.service.core.oauth2.CustomOAuth2UserService;
+import org.apache.streampipes.service.core.oauth2.CustomOidcUserService;
+import
org.apache.streampipes.service.core.oauth2.HttpCookieOAuth2AuthorizationRequestRepository;
+import
org.apache.streampipes.service.core.oauth2.OAuth2AccessTokenResponseConverterWithDefaults;
+import
org.apache.streampipes.service.core.oauth2.OAuth2AuthenticationFailureHandler;
+import
org.apache.streampipes.service.core.oauth2.OAuth2AuthenticationSuccessHandler;
import org.apache.streampipes.user.management.service.SpUserDetailsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -34,22 +46,51 @@ import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
+import
org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
+import
org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import
org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
+import
org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
+import
org.springframework.security.oauth2.client.registration.ClientRegistration;
+import
org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import
org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import
org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.web.SecurityFilterChain;
import
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import
org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true,
jsr250Enabled = true)
public class WebSecurityConfig {
+ private static final Logger LOG =
LoggerFactory.getLogger(WebSecurityConfig.class);
+
private final UserDetailsService userDetailsService;
private final StreamPipesPasswordEncoder passwordEncoder;
+ private final Environment env;
+
+ @Autowired
+ private CustomOAuth2UserService customOAuth2UserService;
+
+ @Autowired
+ CustomOidcUserService customOidcUserService;
+
+ @Autowired
+ private OAuth2AuthenticationSuccessHandler
oAuth2AuthenticationSuccessHandler;
+
+ @Autowired
+ private OAuth2AuthenticationFailureHandler
oAuth2AuthenticationFailureHandler;
public WebSecurityConfig(StreamPipesPasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
this.userDetailsService = new SpUserDetailsService();
+ this.env = Environments.getEnvironment();
}
@Autowired
@@ -59,7 +100,6 @@ public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
-
http
.cors()
.and()
@@ -71,17 +111,46 @@ public class WebSecurityConfig {
.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedRequestEntryPoint())
.and()
- .authorizeHttpRequests((authz) -> authz
- .requestMatchers(UnauthenticatedInterfaces
- .get()
- .stream()
- .map(AntPathRequestMatcher::new)
- .toList()
- .toArray(new AntPathRequestMatcher[0]))
- .permitAll()
- .anyRequest()
- .authenticated().and()
- .addFilterBefore(tokenAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class));
+ .authorizeHttpRequests((authz) -> {
+ try {
+ authz
+ .requestMatchers(UnauthenticatedInterfaces
+ .get()
+ .stream()
+ .map(AntPathRequestMatcher::new)
+ .toList()
+ .toArray(new AntPathRequestMatcher[0]))
+ .permitAll()
+ .anyRequest()
+ .authenticated();
+
+ if (env.getOAuthEnabled().getValueOrDefault()) {
+ LOG.info("Configuring OAuth authentication from environment
variables");
+ authz
+ .and()
+ .oauth2Login()
+ .authorizationEndpoint()
+
.authorizationRequestRepository(cookieOAuth2AuthorizationRequestRepository())
+ .and()
+ .redirectionEndpoint()
+ .and()
+ .userInfoEndpoint()
+ .oidcUserService(customOidcUserService)
+ .userService(customOAuth2UserService)
+ .and()
+ .tokenEndpoint()
+
.accessTokenResponseClient(authorizationCodeTokenResponseClient())
+ .and()
+ .successHandler(oAuth2AuthenticationSuccessHandler)
+ .failureHandler(oAuth2AuthenticationFailureHandler);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+
+ http.addFilterBefore(tokenAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@@ -105,4 +174,62 @@ public class WebSecurityConfig {
return new RequestAttributeSecurityContextRepository();
}
+ @Bean
+ public HttpCookieOAuth2AuthorizationRequestRepository
cookieOAuth2AuthorizationRequestRepository() {
+ return new HttpCookieOAuth2AuthorizationRequestRepository();
+ }
+
+ @Bean
+ public ClientRegistrationRepository clientRegistrationRepository() {
+ var registrations = getRegistrations();
+
+ return new InMemoryClientRegistrationRepository(registrations);
+ }
+
+ private List<ClientRegistration> getRegistrations() {
+ var oauthConfigs = Environments.getEnvironment().getOAuthConfigurations();
+
+ return oauthConfigs.stream().map(config -> {
+ ClientRegistration.Builder builder =
this.getBuilder(config.getRegistrationId());
+ builder.scope(config.getScopes());
+ builder.authorizationUri(config.getAuthorizationUri());
+ builder.tokenUri(config.getTokenUri());
+ builder.jwkSetUri(config.getJwkSetUri());
+ builder.issuerUri(config.getIssuerUri());
+ builder.userInfoUri(config.getUserInfoUri());
+ builder.clientSecret(config.getClientSecret());
+ builder.userNameAttributeName(config.getEmailAttributeName());
+ builder.clientName(config.getClientName());
+ builder.clientId(config.getClientId());
+ return builder.build();
+ }
+ ).toList();
+ }
+
+ protected final ClientRegistration.Builder getBuilder(String registrationId)
{
+ ClientRegistration.Builder builder =
ClientRegistration.withRegistrationId(registrationId);
+
builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+ builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
+ builder.redirectUri(
+
String.format("%s/streampipes-backend/{action}/oauth2/code/{registrationId}",
+ env.getOAuthRedirectUri().getValueOrDefault()
+ )
+ );
+ return builder;
+ }
+
+ private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
authorizationCodeTokenResponseClient() {
+ var tokenResponseHttpMessageConverter = new
OAuth2AccessTokenResponseHttpMessageConverter();
+ tokenResponseHttpMessageConverter
+ .setAccessTokenResponseConverter(new
OAuth2AccessTokenResponseConverterWithDefaults());
+ var restTemplate = new RestTemplate(
+ List.of(new FormHttpMessageConverter(),
tokenResponseHttpMessageConverter)
+ );
+ restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
+ var tokenResponseClient = new
DefaultAuthorizationCodeTokenResponseClient();
+ tokenResponseClient.setRestOperations(restTemplate);
+ return tokenResponseClient;
+
+ }
+
}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/CustomOAuth2UserService.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/CustomOAuth2UserService.java
new file mode 100755
index 0000000000..7715d1c668
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/CustomOAuth2UserService.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import
org.apache.streampipes.rest.security.OAuth2AuthenticationProcessingException;
+
+import org.springframework.security.core.AuthenticationException;
+import
org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+
+@Service
+public class CustomOAuth2UserService extends DefaultOAuth2UserService {
+
+ @Override
+ public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws
OAuth2AuthenticationException {
+ OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
+ try {
+ var attributes = new HashMap<>(oAuth2User.getAttributes());
+ var provider =
oAuth2UserRequest.getClientRegistration().getRegistrationId();
+ return new UserService().processUserRegistration(provider, attributes);
+ } catch (AuthenticationException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new OAuth2AuthenticationProcessingException(e.getMessage(),
e.getCause());
+ }
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/CustomOidcUserService.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/CustomOidcUserService.java
new file mode 100755
index 0000000000..881b333379
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/CustomOidcUserService.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+
+import
org.apache.streampipes.rest.security.OAuth2AuthenticationProcessingException;
+
+import org.springframework.security.core.AuthenticationException;
+import
org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import
org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CustomOidcUserService extends OidcUserService {
+
+ @Override
+ public OidcUser loadUser(OidcUserRequest userRequest) throws
OAuth2AuthenticationException {
+ OidcUser oidcUser = super.loadUser(userRequest);
+ try {
+ var provider = userRequest.getClientRegistration().getRegistrationId();
+ return new UserService().processUserRegistration(
+ provider,
+ oidcUser.getAttributes(),
+ oidcUser.getIdToken(),
+ oidcUser.getUserInfo()
+ );
+ } catch (AuthenticationException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new OAuth2AuthenticationProcessingException(e.getMessage(),
e.getCause());
+ }
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java
new file mode 100755
index 0000000000..2658703e04
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import org.apache.streampipes.service.core.oauth2.util.CookieUtils;
+
+import com.nimbusds.oauth2.sdk.util.StringUtils;
+
+import
org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
+import
org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+
+@Component
+public class HttpCookieOAuth2AuthorizationRequestRepository
+ implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
+
+ private static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME =
"oauth2_auth_request";
+ public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
+ private static final int cookieExpireSeconds = 180;
+
+ @Override
+ public OAuth2AuthorizationRequest
loadAuthorizationRequest(HttpServletRequest request) {
+ return CookieUtils.getCookie(
+ request,
+ OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME
+ ).map(cookie -> CookieUtils.deserialize(cookie,
OAuth2AuthorizationRequest.class))
+ .orElse(null);
+ }
+
+ @Override
+ public void saveAuthorizationRequest(OAuth2AuthorizationRequest
authorizationRequest,
+ HttpServletRequest request,
+ HttpServletResponse response) {
+ if (authorizationRequest == null) {
+ CookieUtils.deleteCookie(request, response,
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
+ CookieUtils.deleteCookie(request, response,
REDIRECT_URI_PARAM_COOKIE_NAME);
+ return;
+ }
+
+ CookieUtils.addCookie(
+ response,
+ OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
+ CookieUtils.serialize(authorizationRequest),
+ cookieExpireSeconds
+ );
+
+ String redirectUriAfterLogin =
request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
+ if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
+ CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME,
redirectUriAfterLogin, cookieExpireSeconds);
+ }
+ }
+
+ @Override
+ public OAuth2AuthorizationRequest
removeAuthorizationRequest(HttpServletRequest request,
+
HttpServletResponse response) {
+ return this.loadAuthorizationRequest(request);
+ }
+
+ public void removeAuthorizationRequestCookies(HttpServletRequest request,
+ HttpServletResponse response) {
+ CookieUtils.deleteCookie(request, response,
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
+ CookieUtils.deleteCookie(request, response,
REDIRECT_URI_PARAM_COOKIE_NAME);
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AccessTokenResponseConverterWithDefaults.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AccessTokenResponseConverterWithDefaults.java
new file mode 100755
index 0000000000..b10ca8681d
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AccessTokenResponseConverterWithDefaults.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import
org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class OAuth2AccessTokenResponseConverterWithDefaults
+ implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> {
+ private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = Stream
+ .of(
+ OAuth2ParameterNames.ACCESS_TOKEN,
+ OAuth2ParameterNames.TOKEN_TYPE,
+ OAuth2ParameterNames.EXPIRES_IN,
+ OAuth2ParameterNames.REFRESH_TOKEN,
+ OAuth2ParameterNames.SCOPE
+ )
+ .collect(Collectors.toSet());
+
+ private final OAuth2AccessToken.TokenType defaultAccessTokenType =
OAuth2AccessToken.TokenType.BEARER;
+
+ @Override
+ public OAuth2AccessTokenResponse convert(Map<String, Object>
tokenResponseParameters) {
+ var accessToken =
tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
+
+ OAuth2AccessToken.TokenType accessTokenType = this.defaultAccessTokenType;
+ var tokenType = OAuth2AccessToken.TokenType.BEARER.getValue();
+ if (tokenType.equalsIgnoreCase((String)
tokenResponseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) {
+ accessTokenType = OAuth2AccessToken.TokenType.BEARER;
+ }
+
+ long expiresIn = 0;
+ if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) {
+ try {
+ expiresIn = (int)
tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+
+ Set<String> scopes = Collections.emptySet();
+ if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
+ var scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
+ scopes = Arrays
+ .stream(StringUtils.delimitedListToStringArray((String) scope, " "))
+ .collect(Collectors.toSet());
+ }
+
+ Map<String, Object> additionalParameters = new LinkedHashMap<>();
+ tokenResponseParameters
+ .entrySet()
+ .stream().filter(e ->
!TOKEN_RESPONSE_PARAMETER_NAMES.contains(e.getKey()))
+ .forEach(e -> additionalParameters.put(e.getKey(), e.getValue()));
+
+ return OAuth2AccessTokenResponse
+ .withToken((String) accessToken)
+ .tokenType(accessTokenType)
+ .expiresIn(expiresIn)
+ .scopes(scopes)
+ .additionalParameters(additionalParameters)
+ .build();
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AuthenticationFailureHandler.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AuthenticationFailureHandler.java
new file mode 100755
index 0000000000..fa86718cdb
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AuthenticationFailureHandler.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import org.apache.streampipes.service.core.oauth2.util.CookieUtils;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.AuthenticationException;
+import
org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+import static
org.apache.streampipes.service.core.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME;
+
+
+@Component
+public class OAuth2AuthenticationFailureHandler extends
SimpleUrlAuthenticationFailureHandler {
+
+ @Autowired
+ HttpCookieOAuth2AuthorizationRequestRepository
httpCookieOAuth2AuthorizationRequestRepository;
+
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException exception)
throws IOException {
+ String targetUrl = CookieUtils
+ .getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
+ .map(Cookie::getValue)
+ .orElse(("/"));
+
+ targetUrl = UriComponentsBuilder
+ .fromUriString(targetUrl)
+ .queryParam("error", exception.getLocalizedMessage())
+ .build()
+ .toUriString();
+
+
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request,
response);
+
+ getRedirectStrategy().sendRedirect(request, response, targetUrl);
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AuthenticationSuccessHandler.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AuthenticationSuccessHandler.java
new file mode 100755
index 0000000000..43d058a992
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OAuth2AuthenticationSuccessHandler.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import org.apache.streampipes.commons.environment.Environment;
+import org.apache.streampipes.commons.environment.Environments;
+import org.apache.streampipes.rest.shared.exception.BadRequestException;
+import org.apache.streampipes.service.core.oauth2.util.CookieUtils;
+import org.apache.streampipes.user.management.jwt.JwtTokenProvider;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import
org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Optional;
+
+import static
org.apache.streampipes.service.core.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME;
+
+@Component
+public class OAuth2AuthenticationSuccessHandler extends
SimpleUrlAuthenticationSuccessHandler {
+
+ private final JwtTokenProvider tokenProvider;
+ private final HttpCookieOAuth2AuthorizationRequestRepository
httpCookieOAuth2AuthorizationRequestRepository;
+ private final Environment env;
+
+ @Autowired
+
OAuth2AuthenticationSuccessHandler(HttpCookieOAuth2AuthorizationRequestRepository
+
httpCookieOAuth2AuthorizationRequestRepository) {
+ this.tokenProvider = new JwtTokenProvider();
+ this.httpCookieOAuth2AuthorizationRequestRepository =
httpCookieOAuth2AuthorizationRequestRepository;
+ this.env = Environments.getEnvironment();
+ }
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request,
+ HttpServletResponse response,
+ Authentication authentication) throws
IOException {
+ String targetUrl = determineTargetUrl(request, response, authentication);
+
+ if (response.isCommitted()) {
+ return;
+ }
+
+ clearAuthenticationAttributes(request, response);
+ getRedirectStrategy().sendRedirect(request, response, targetUrl);
+ }
+
+ @Override
+ protected String determineTargetUrl(HttpServletRequest request,
+ HttpServletResponse response,
+ Authentication authentication) {
+ Optional<String> redirectUri = CookieUtils
+ .getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
+ .map(Cookie::getValue);
+
+ if (redirectUri.isPresent() &&
!isAuthorizedRedirectUri(redirectUri.get())) {
+ throw new BadRequestException(
+ "Unauthorized redirect uri found - check the redirect uri in your
OAuth config"
+ );
+ }
+
+ String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
+ String token = tokenProvider.createToken(authentication);
+
+ return targetUrl + "?token=" + token;
+ }
+
+ protected void clearAuthenticationAttributes(HttpServletRequest request,
+ HttpServletResponse response) {
+ super.clearAuthenticationAttributes(request);
+
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request,
response);
+ }
+
+ private boolean isAuthorizedRedirectUri(String uri) {
+ URI clientRedirectUri = URI.create(uri);
+ var authorizedRedirectUri = env.getOAuthRedirectUri();
+ if (authorizedRedirectUri.exists()) {
+ URI authorizedURI = URI.create(authorizedRedirectUri.getValue());
+ return
authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
+ && authorizedURI.getPort() == clientRedirectUri.getPort();
+ } else {
+ return false;
+ }
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OidcUserAccountDetails.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OidcUserAccountDetails.java
new file mode 100755
index 0000000000..48b3fd5c4f
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/OidcUserAccountDetails.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import org.apache.streampipes.model.client.user.UserAccount;
+import org.apache.streampipes.user.management.model.UserAccountDetails;
+
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.util.Map;
+
+public class OidcUserAccountDetails extends UserAccountDetails implements
OAuth2User, OidcUser {
+
+ private final OidcIdToken idToken;
+ private final OidcUserInfo userInfo;
+ private Map<String, Object> attributes;
+
+ public OidcUserAccountDetails(UserAccount user,
+ OidcIdToken idToken,
+ OidcUserInfo userInfo) {
+ super(user);
+ this.idToken = idToken;
+ this.userInfo = userInfo;
+ }
+
+ public static OidcUserAccountDetails create(UserAccount user,
+ Map<String, Object> attributes,
+ OidcIdToken idToken,
+ OidcUserInfo userInfo) {
+ OidcUserAccountDetails localUser = new OidcUserAccountDetails(user,
idToken, userInfo);
+ localUser.setAttributes(attributes);
+ return localUser;
+ }
+
+ public void setAttributes(Map<String, Object> attributes) {
+ this.attributes = attributes;
+ }
+
+ @Override
+ public String getName() {
+ return this.details.getFullName();
+ }
+
+ @Override
+ public Map<String, Object> getAttributes() {
+ return this.attributes;
+ }
+
+ @Override
+ public Map<String, Object> getClaims() {
+ return this.attributes;
+ }
+
+ @Override
+ public OidcUserInfo getUserInfo() {
+ return this.userInfo;
+ }
+
+ @Override
+ public OidcIdToken getIdToken() {
+ return this.idToken;
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
new file mode 100755
index 0000000000..1b78b67238
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2;
+
+import org.apache.streampipes.commons.environment.Environment;
+import org.apache.streampipes.commons.environment.Environments;
+import org.apache.streampipes.model.client.user.Role;
+import org.apache.streampipes.model.client.user.UserAccount;
+import org.apache.streampipes.resource.management.UserResourceManager;
+import
org.apache.streampipes.rest.security.OAuth2AuthenticationProcessingException;
+import org.apache.streampipes.storage.api.IUserStorage;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class UserService {
+
+ private final IUserStorage userStorage;
+ private final Environment env;
+
+ public UserService() {
+ this.userStorage =
StorageDispatcher.INSTANCE.getNoSqlStore().getUserStorageAPI();
+ this.env = Environments.getEnvironment();
+ }
+
+ public OidcUserAccountDetails processUserRegistration(String registrationId,
+ Map<String, Object>
attributes) {
+ return processUserRegistration(registrationId, attributes, null, null);
+ }
+
+ public OidcUserAccountDetails processUserRegistration(String registrationId,
+ Map<String, Object>
attributes,
+ OidcIdToken idToken,
+ OidcUserInfo userInfo)
{
+ var oAuthConfigOpt = env.getOAuthConfigurations()
+ .stream()
+ .filter(c -> c.getRegistrationId().equals(registrationId))
+ .findFirst();
+
+ if (oAuthConfigOpt.isPresent()) {
+ var oAuthConfig = oAuthConfigOpt.get();
+ var principalId =
attributes.get(oAuthConfig.getUserIdAttributeName()).toString();
+ var fullName = attributes.get(oAuthConfig.getFullNameAttributeName());
+ if (oAuthConfig.getEmailAttributeName().isEmpty()) {
+ throw new OAuth2AuthenticationProcessingException("Email attribute key
not found in attributes");
+ }
+ var email =
attributes.get(oAuthConfig.getEmailAttributeName()).toString();
+ UserAccount user = (UserAccount) userStorage.getUserById(principalId);
+ if (user != null) {
+ if (!user.getProvider().equals(registrationId) &&
!user.getProvider().equals(UserAccount.LOCAL)) {
+ throw new OAuth2AuthenticationProcessingException(
+ String.format("Already signed up with another provider %s",
user.getProvider())
+ );
+ }
+ } else {
+ new
UserResourceManager().registerOauthUser(toUserAccount(registrationId,
principalId, email, fullName));
+ user = (UserAccount) userStorage.getUserById(principalId);
+ }
+ return OidcUserAccountDetails.create(user, attributes, idToken,
userInfo);
+ } else {
+ throw new OAuth2AuthenticationProcessingException(
+ String.format("No config found for provider %s", registrationId)
+ );
+ }
+ }
+
+ private UserAccount toUserAccount(String registrationId,
+ String principalId,
+ String email,
+ Object fullName) {
+ List<Role> roles =
Stream.of(Role.ROLE_ADMIN.toString()).map(Role::valueOf).toList();
+ var user = UserAccount.from(email, null, new HashSet<>(roles));
+ user.setPrincipalId(principalId);
+ if (Objects.nonNull(fullName)) {
+ user.setFullName(fullName.toString());
+ }
+ user.setAccountEnabled(false);
+ user.setProvider(registrationId);
+ return user;
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/util/CookieUtils.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/util/CookieUtils.java
new file mode 100755
index 0000000000..00a1312766
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/util/CookieUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.streampipes.service.core.oauth2.util;
+
+import org.springframework.util.SerializationUtils;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.util.Base64;
+import java.util.Optional;
+
+public class CookieUtils {
+
+ public static Optional<Cookie> getCookie(HttpServletRequest request,
+ String name) {
+ Cookie[] cookies = request.getCookies();
+
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(name)) {
+ return Optional.of(cookie);
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ public static void addCookie(HttpServletResponse response,
+ String name,
+ String value,
+ int maxAge) {
+ Cookie cookie = new Cookie(name, value);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(maxAge);
+ response.addCookie(cookie);
+ }
+
+ public static void deleteCookie(HttpServletRequest request,
+ HttpServletResponse response,
+ String name) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(name)) {
+ cookie.setValue("");
+ cookie.setPath("/");
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+ }
+ }
+ }
+ }
+
+ public static String serialize(Object object) {
+ return
Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));
+ }
+
+ public static <T> T deserialize(Cookie cookie, Class<T> clazz) {
+ return
clazz.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
+ }
+}
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
index 6f7f04a72c..e6a39f53cb 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
@@ -16,12 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
+
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2024-04-22
14:35:38.
+// Generated using typescript-generator version 3.2.1263 on 2024-07-29
21:20:28.
+
+import { Storable } from '@streampipes/platform-services';
-export class Group {
+export class Group implements Storable {
+ elementId: string;
groupId: string;
groupName: string;
rev: string;
@@ -32,6 +36,7 @@ export class Group {
return data;
}
const instance = target || new Group();
+ instance.elementId = data.elementId;
instance.groupId = data.groupId;
instance.groupName = data.groupName;
instance.rev = data.rev;
@@ -66,7 +71,8 @@ export class MatchingResultMessage {
}
}
-export class Permission {
+export class Permission implements Storable {
+ elementId: string;
grantedAuthorities: PermissionEntry[];
objectClassName: string;
objectInstanceId: string;
@@ -80,6 +86,7 @@ export class Permission {
return data;
}
const instance = target || new Permission();
+ instance.elementId = data.elementId;
instance.grantedAuthorities = __getCopyArrayFn(
PermissionEntry.fromData,
)(data.grantedAuthorities);
@@ -193,6 +200,7 @@ export class UserAccount extends Principal {
preferredDataProcessors: string[];
preferredDataSinks: string[];
preferredDataStreams: string[];
+ provider: string;
userApiTokens: UserApiToken[];
static fromData(data: UserAccount, target?: UserAccount): UserAccount {
@@ -214,6 +222,7 @@ export class UserAccount extends Principal {
instance.preferredDataStreams = __getCopyArrayFn(__identity<string>())(
data.preferredDataStreams,
);
+ instance.provider = data.provider;
instance.userApiTokens = __getCopyArrayFn(UserApiToken.fromData)(
data.userApiTokens,
);
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index cf79d51045..29b7a7f837 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -20,7 +20,7 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2024-06-30
09:10:19.
+// Generated using typescript-generator version 3.2.1263 on 2024-07-29
21:03:44.
export class NamedStreamPipesEntity implements Storable {
'@class':
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.html
b/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.html
new file mode 100644
index 0000000000..334eb9acc3
--- /dev/null
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.html
@@ -0,0 +1,21 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<div fxLayout="row" fxFlex="100" class="warning">
+ <ng-content></ng-content>
+</div>
diff --git
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
b/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.scss
similarity index 82%
copy from
streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
copy to
ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.scss
index 5457645e38..73ce449cf2 100644
---
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
@@ -16,9 +16,10 @@
*
*/
-package org.apache.streampipes.model.client.user;
-
-import java.util.List;
-
-public record UserRegistrationData(String username, String password,
List<String> roles) {
+.warning {
+ border: 1px solid #dea843;
+ background: var(--color-bg-2);
+ padding: 5px;
+ margin-top: 10px;
+ margin-bottom: 10px;
}
diff --git
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
b/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.ts
similarity index 75%
copy from
streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
copy to
ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.ts
index 5457645e38..77407d9551 100644
---
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserRegistrationData.java
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/warning-box/warning-box.component.ts
@@ -16,9 +16,14 @@
*
*/
-package org.apache.streampipes.model.client.user;
+import { Component, Input } from '@angular/core';
-import java.util.List;
-
-public record UserRegistrationData(String username, String password,
List<String> roles) {
+@Component({
+ selector: 'sp-warning-box',
+ templateUrl: './warning-box.component.html',
+ styleUrls: ['./warning-box.component.scss'],
+})
+export class SpWarningBoxComponent {
+ @Input()
+ color = '';
}
diff --git a/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
b/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
index a9131bf1d4..e2e7555e1c 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
+++ b/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
@@ -43,6 +43,7 @@ import { MatTableModule } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { SpExceptionDetailsComponent } from
'./components/sp-exception-message/exception-details/exception-details.component';
+import { SpWarningBoxComponent } from
'./components/warning-box/warning-box.component';
@NgModule({
declarations: [
@@ -59,6 +60,7 @@ import { SpExceptionDetailsComponent } from
'./components/sp-exception-message/e
SpLabelComponent,
SpTableComponent,
SplitSectionComponent,
+ SpWarningBoxComponent,
],
imports: [
CommonModule,
@@ -89,6 +91,7 @@ import { SpExceptionDetailsComponent } from
'./components/sp-exception-message/e
SpLabelComponent,
SpTableComponent,
SplitSectionComponent,
+ SpWarningBoxComponent,
],
})
export class SharedUiModule {}
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index d954892499..93bd254712 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -36,6 +36,7 @@ export * from
'./lib/components/sp-exception-message/exception-details-dialog/ex
export * from
'./lib/components/sp-exception-message/exception-details/exception-details.component';
export * from './lib/components/sp-label/sp-label.component';
export * from './lib/components/sp-table/sp-table.component';
+export * from './lib/components/warning-box/warning-box.component';
export * from './lib/models/sp-navigation.model';
diff --git
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
index b28837bc03..f52a07bccd 100644
---
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
+++
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
@@ -19,6 +19,9 @@
<div class="sp-dialog-container">
<div class="sp-dialog-content">
<div fxFlex="100" fxLayout="column" class="p-15">
+ <sp-warning-box *ngIf="isUserAccount && isExternalProvider">
+ Some settings of externally-managed users cannot be changed.
+ </sp-warning-box>
<form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
<div class="general-options-panel" fxLayout="column">
<span class="general-options-header">Basics</span>
@@ -41,7 +44,6 @@
fxFlex
type="email"
matInput
- required
data-cy="new-user-email"
/>
<mat-error>Must be a valid email address.</mat-error>
@@ -56,7 +58,6 @@
formControlName="fullName"
fxFlex
matInput
- required
data-cy="new-user-full-name"
/>
</mat-form-field>
diff --git
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
index 03fa4641bd..170b85d2e7 100644
---
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
+++
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
@@ -57,6 +57,7 @@ export class EditUserDialogComponent implements OnInit {
editMode: boolean;
isUserAccount: boolean;
+ isExternalProvider: boolean = false;
parentForm: UntypedFormGroup;
clonedUser: UserAccount | ServiceAccount;
@@ -102,20 +103,24 @@ export class EditUserDialogComponent implements OnInit {
? UserAccount.fromData(this.user, new UserAccount())
: ServiceAccount.fromData(this.user, new ServiceAccount());
this.isUserAccount = this.user instanceof UserAccount;
+ this.isExternalProvider =
+ this.user instanceof UserAccount && this.user.provider !== 'local';
this.parentForm = this.fb.group({});
+ let usernameValidators = [];
+ if (this.isUserAccount) {
+ if ((this.clonedUser as UserAccount).provider === 'local') {
+ usernameValidators = [Validators.required, Validators.email];
+ } else {
+ usernameValidators = [Validators.email];
+ }
+ } else {
+ usernameValidators = [Validators.required];
+ }
this.parentForm.addControl(
'username',
- new UntypedFormControl(
- this.clonedUser.username,
- Validators.required,
- ),
+ new UntypedFormControl(this.clonedUser.username),
);
- if (this.isUserAccount) {
- this.parentForm.controls['username'].setValidators([
- Validators.required,
- Validators.email,
- ]);
- }
+ this.parentForm.controls['username'].setValidators(usernameValidators);
this.parentForm.addControl(
'accountEnabled',
new UntypedFormControl(this.clonedUser.accountEnabled),
@@ -158,6 +163,11 @@ export class EditUserDialogComponent implements OnInit {
this.parentForm.setValidators(this.checkPasswords);
}
+ if (this.isExternalProvider) {
+ this.parentForm.controls['username'].disable();
+ this.parentForm.controls['fullName'].disable();
+ }
+
this.parentForm.valueChanges.subscribe(v => {
this.clonedUser.username = v.username;
this.clonedUser.accountLocked = v.accountLocked;
diff --git
a/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html
b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html
index 492ca0b830..7f3e749bd7 100644
---
a/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html
+++
b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html
@@ -47,6 +47,19 @@
<h4 style="margin-bottom: 0px">{{ account.username }}</h4>
</td>
</ng-container>
+ <ng-container matColumnDef="provider">
+ <th mat-header-cell mat-sort-header *matHeaderCellDef>Type</th>
+ <td
+ mat-cell
+ *matCellDef="let account"
+ data-cy="user-provider-table-row"
+ >
+ <sp-label
+ [small]="true"
+ [labelText]="account.provider"
+ ></sp-label>
+ </td>
+ </ng-container>
<ng-container matColumnDef="fullName">
<th mat-header-cell mat-sort-header *matHeaderCellDef>
diff --git
a/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
index 3f377904a6..32c74249fc 100644
---
a/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
+++
b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
@@ -27,7 +27,7 @@ import { Observable } from 'rxjs';
styleUrls: ['./security-user-config.component.scss'],
})
export class SecurityUserConfigComponent extends
AbstractSecurityPrincipalConfig<UserAccount> {
- displayedColumns: string[] = ['username', 'fullName', 'edit'];
+ displayedColumns: string[] = ['username', 'provider', 'fullName', 'edit'];
getObservable(): Observable<UserAccount[]> {
return this.userAdminService.getAllUserAccounts();
@@ -38,6 +38,8 @@ export class SecurityUserConfigComponent extends
AbstractSecurityPrincipalConfig
}
getNewInstance(): UserAccount {
- return new UserAccount();
+ const user = new UserAccount();
+ user.provider = 'local';
+ return user;
}
}
diff --git a/ui/src/app/login/components/login/login.component.html
b/ui/src/app/login/components/login/login.component.html
index 5cd150ce02..a9a1976609 100644
--- a/ui/src/app/login/components/login/login.component.html
+++ b/ui/src/app/login/components/login/login.component.html
@@ -47,13 +47,13 @@
/>
</mat-form-field>
</div>
- <div class="form-actions" style="margin-top: 20px">
+ <div class="form-actions">
<button
mat-button
mat-raised-button
color="accent"
data-cy="login-button"
- (click)="logIn()"
+ (click)="doLogin()"
[disabled]="!parentForm.valid || loading"
>
<span *ngIf="loading">Logging in...</span>
@@ -91,6 +91,35 @@
<a [routerLink]="['/register']">Create new account</a>
</div>
</div>
+ <div
+ fxLayout="column"
+ class="mt-10"
+ *ngIf="loginSettings.oAuthSettings?.enabled"
+ >
+ <div class="separator">
+ <span>or</span>
+ </div>
+ <div
+ fxLayout="column"
+ *ngFor="
+ let provider of loginSettings.oAuthSettings
+ .supportedProviders
+ "
+ class="mt-10"
+ >
+ <button
+ mat-button
+ mat-raised-button
+ color="accent"
+ data-cy="login-button"
+ (click)="doOAuthLogin(provider.registrationId)"
+ >
+ <span *ngIf="!loading"
+ >Login with {{ provider.name }}</span
+ >
+ </button>
+ </div>
+ </div>
</div>
</form>
</div>
diff --git a/ui/src/app/login/components/login/login.component.scss
b/ui/src/app/login/components/login/login.component.scss
index 3b4edba73f..6d899a1e69 100644
--- a/ui/src/app/login/components/login/login.component.scss
+++ b/ui/src/app/login/components/login/login.component.scss
@@ -37,3 +37,16 @@
background: #a2ffa2;
color: #3e3e3e;
}
+
+.separator {
+ width: 100%;
+ text-align: center;
+ border-bottom: 1px solid var(--color-bg-3);
+ line-height: 0.1em;
+ margin: 20px 0 20px;
+}
+
+.separator span {
+ background: #fff;
+ padding: 0 10px;
+}
diff --git a/ui/src/app/login/components/login/login.component.ts
b/ui/src/app/login/components/login/login.component.ts
index af50a53337..e2cc07b2a5 100644
--- a/ui/src/app/login/components/login/login.component.ts
+++ b/ui/src/app/login/components/login/login.component.ts
@@ -54,7 +54,7 @@ export class LoginComponent extends BaseLoginPageDirective {
this.credentials = {};
}
- logIn() {
+ doLogin() {
this.authenticationFailed = false;
this.loading = true;
this.loginService.login(this.credentials).subscribe(
@@ -73,6 +73,12 @@ export class LoginComponent extends BaseLoginPageDirective {
}
onSettingsAvailable(): void {
+ const token = this.route.snapshot.queryParamMap.get('token');
+ if (token) {
+ this.authService.oauthLogin(token);
+ this.loading = false;
+ this.router.navigate(['']);
+ }
this.parentForm = this.fb.group({});
this.parentForm.addControl(
'username',
@@ -89,4 +95,8 @@ export class LoginComponent extends BaseLoginPageDirective {
});
this.returnUrl = this.route.snapshot.queryParams.returnUrl || '';
}
+
+ doOAuthLogin(provider: string): void {
+ window.location.href =
`/streampipes-backend/oauth2/authorization/${provider}?redirect_uri=${this.loginSettings.oAuthSettings.redirectUri}/%23/login`;
+ }
}
diff --git a/ui/src/app/login/components/login/login.model.ts
b/ui/src/app/login/components/login/login.model.ts
index fc28a3a4fc..a56f76c941 100644
--- a/ui/src/app/login/components/login/login.model.ts
+++ b/ui/src/app/login/components/login/login.model.ts
@@ -18,8 +18,20 @@
import { LinkSettings } from '@streampipes/platform-services';
+export interface OAuthProvider {
+ name: string;
+ registrationId: string;
+}
+
+export interface OAuthSettings {
+ enabled: boolean;
+ redirectUri: string;
+ supportedProviders: OAuthProvider[];
+}
+
export interface LoginModel {
allowSelfRegistration: boolean;
allowPasswordRecovery: boolean;
linkSettings: LinkSettings;
+ oAuthSettings: OAuthSettings;
}
diff --git
a/ui/src/app/profile/components/general/general-profile-settings.component.html
b/ui/src/app/profile/components/general/general-profile-settings.component.html
index 78c3a35074..778dc7abb0 100644
---
a/ui/src/app/profile/components/general/general-profile-settings.component.html
+++
b/ui/src/app/profile/components/general/general-profile-settings.component.html
@@ -27,9 +27,13 @@
title="Main Settings"
subtitle="Manage your basic profile settings here."
>
+ <sp-warning-box *ngIf="isExternalUser">
+ Settings for externally-managed users can't be changed.
+ </sp-warning-box>
<div fxLayout="row" fxLayoutAlign="start center">
<span>{{ userData.username }}</span>
<button
+ [disabled]="isExternalUser"
mat-button
mat-raised-button
color="accent"
@@ -41,10 +45,15 @@
</div>
<mat-form-field fxFlex color="accent" class="mt-10 mb-10">
<mat-label>Full Name</mat-label>
- <input [(ngModel)]="userData.fullName" matInput />
+ <input
+ [disabled]="isExternalUser"
+ [(ngModel)]="userData.fullName"
+ matInput
+ />
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="start center">
<button
+ [disabled]="isExternalUser"
mat-button
mat-raised-button
color="accent"
@@ -53,6 +62,7 @@
Update profile
</button>
<button
+ [disabled]="isExternalUser"
mat-button
mat-raised-button
color="accent"
diff --git
a/ui/src/app/profile/components/general/general-profile-settings.component.ts
b/ui/src/app/profile/components/general/general-profile-settings.component.ts
index dde8cd92f4..b0341a5d87 100644
---
a/ui/src/app/profile/components/general/general-profile-settings.component.ts
+++
b/ui/src/app/profile/components/general/general-profile-settings.component.ts
@@ -43,6 +43,7 @@ export class GeneralProfileSettingsComponent
darkMode = false;
originalDarkMode = false;
darkModeChanged = false;
+ isExternalUser = false;
constructor(
authService: AuthService,
@@ -75,6 +76,7 @@ export class GeneralProfileSettingsComponent
onUserDataReceived() {
this.originalDarkMode = this.userData.darkMode;
this.currentUserService.darkMode$.next(this.userData.darkMode);
+ this.isExternalUser = this.userData.provider !== 'local';
}
updateAppearanceMode() {
diff --git a/ui/src/app/services/auth.service.ts
b/ui/src/app/services/auth.service.ts
index cee23ba2c9..582c317fc9 100644
--- a/ui/src/app/services/auth.service.ts
+++ b/ui/src/app/services/auth.service.ts
@@ -59,6 +59,15 @@ export class AuthService {
this.currentUserService.user$.next(decodedToken.user);
}
+ public oauthLogin(token: string) {
+ const jwtHelper: JwtHelperService = new JwtHelperService({});
+ const decodedToken = jwtHelper.decodeToken(token);
+ this.tokenStorage.saveToken(token);
+ this.tokenStorage.saveUser(decodedToken.user);
+ this.currentUserService.authToken$.next(token);
+ this.currentUserService.user$.next(decodedToken.user);
+ }
+
public logout() {
this.tokenStorage.clearTokens();
this.currentUserService.authToken$.next(undefined);