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);

Reply via email to