You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2021/01/05 16:13:24 UTC

[incubator-streampipes] branch STREAMPIPES-272 updated: [STREAMPIPES-276] Add simple token-based API authentication

This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch STREAMPIPES-272
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git


The following commit(s) were added to refs/heads/STREAMPIPES-272 by this push:
     new 87a47b5  [STREAMPIPES-276] Add simple token-based API authentication
87a47b5 is described below

commit 87a47b51512c5e8ca6f6cdf52b0b9d00798c1a52
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Tue Jan 5 17:13:05 2021 +0100

    [STREAMPIPES-276] Add simple token-based API authentication
---
 pom.xml                                            |  2 +-
 streampipes-backend/src/main/resources/shiro.ini   |  4 +-
 streampipes-model-client/pom.xml                   |  8 +-
 .../model/client/user/RawUserApiToken.java         | 65 +++++++++++++++
 .../apache/streampipes/model/client/user/User.java | 32 ++++++++
 .../model/client/user/UserApiToken.java            | 62 ++++++++++++++
 .../org/apache/streampipes/rest/impl/User.java     | 23 ++++++
 .../storage/couchdb/impl/UserStorage.java          | 16 ++--
 streampipes-user-management/pom.xml                | 11 ++-
 .../authentication/StreamPipesRealm.java           | 40 +++++----
 .../user/management/service/TokenService.java      | 61 ++++++++++++++
 .../user/management/util/TokenUtil.java            | 62 ++++++++++++++
 .../user/management/util/TestTokenUtil.java        | 57 +++++++++++++
 ui/deployment/app-routing.module.mst               |  6 +-
 ui/deployment/appng5.module.mst                    |  4 +-
 ui/src/app/app-routing.module.ts                   |  6 +-
 .../app/core-model/gen/streampipes-model-client.ts | 94 +++++++++++++++++++++-
 ui/src/app/core-model/gen/streampipes-model.ts     | 18 +++++
 .../core/components/toolbar/toolbar.component.html | 10 ++-
 .../core/components/toolbar/toolbar.component.scss | 13 ++-
 .../core/components/toolbar/toolbar.component.ts   | 11 ++-
 .../profile/components/basic-profile-settings.ts   | 58 +++++++++++++
 .../general-profile-settings.component.html        | 51 ++++++++++++
 .../general-profile-settings.component.scss}       | 19 ++++-
 .../general/general-profile-settings.component.ts} | 26 +++++-
 .../token/token-management-settings.component.html | 90 +++++++++++++++++++++
 .../token-management-settings.component.scss}      | 59 +++++++++++++-
 .../token/token-management-settings.component.ts   | 70 ++++++++++++++++
 ui/src/app/profile/profile.component.html          | 42 ++++++++++
 .../profile.component.scss}                        |  7 +-
 .../profile.component.ts}                          | 21 ++++-
 ui/src/app/profile/profile.module.ts               | 62 ++++++++++++++
 ui/src/app/profile/profile.service.ts              | 53 ++++++++++++
 33 files changed, 1108 insertions(+), 55 deletions(-)

diff --git a/pom.xml b/pom.xml
index 719fd47..7735bd5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
         <rdf4j.version>3.5.0</rdf4j.version>
         <rendersnake.version>1.8</rendersnake.version>
         <retrofit.version>2.5.0</retrofit.version>
-        <shiro.version>1.2.3</shiro.version>
+        <shiro.version>1.7.0</shiro.version>
         <siddhi.version>5.1.12</siddhi.version>
         <slf4j.version>1.7.30</slf4j.version>
         <snakeyaml.version>1.26</snakeyaml.version>
diff --git a/streampipes-backend/src/main/resources/shiro.ini b/streampipes-backend/src/main/resources/shiro.ini
index f6f60c6..50aaf79 100644
--- a/streampipes-backend/src/main/resources/shiro.ini
+++ b/streampipes-backend/src/main/resources/shiro.ini
@@ -72,5 +72,5 @@ securityManager.rememberMeManager.cookie.maxAge = 1000000000
 /api/v2/connect/*/master/sources/*/streams = anon
 /api/v2/connect/*/master/sources/*/streams/* = anon
 /api/v2/connect/*/master/resolvable/*/configurations = anon
-/api/** = customFilter
-/** = customFilter
\ No newline at end of file
+/api/** = authcBearer, customFilter
+/** = authcBearer, customFilter
diff --git a/streampipes-model-client/pom.xml b/streampipes-model-client/pom.xml
index 3832689..8d72daf 100644
--- a/streampipes-model-client/pom.xml
+++ b/streampipes-model-client/pom.xml
@@ -37,6 +37,10 @@
 
 		<!-- External dependencies -->
 		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-annotations</artifactId>
+		</dependency>
+		<dependency>
 			<groupId>com.google.code.gson</groupId>
 			<artifactId>gson</artifactId>
 		</dependency>
@@ -51,7 +55,7 @@
 			<plugin>
 				<groupId>cz.habarta.typescript-generator</groupId>
 				<artifactId>typescript-generator-maven-plugin</artifactId>
-				<version>2.24.612</version>
+				<version>2.27.744</version>
 				<executions>
 					<execution>
 						<id>generate</id>
@@ -87,4 +91,4 @@
 			</plugin>
 		</plugins>
 	</build>
-</project>
\ No newline at end of file
+</project>
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/RawUserApiToken.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/RawUserApiToken.java
new file mode 100644
index 0000000..d70df9e
--- /dev/null
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/RawUserApiToken.java
@@ -0,0 +1,65 @@
+/*
+ * 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.model.client.user;
+
+import org.apache.streampipes.model.shared.annotation.TsModel;
+
+@TsModel
+public class RawUserApiToken {
+
+  private String rawToken;
+  private String hashedToken;
+  private String tokenName;
+  private String tokenId;
+
+  public RawUserApiToken() {
+
+  }
+
+  public String getRawToken() {
+    return rawToken;
+  }
+
+  public void setRawToken(String rawToken) {
+    this.rawToken = rawToken;
+  }
+
+  public String getTokenName() {
+    return tokenName;
+  }
+
+  public void setTokenName(String tokenName) {
+    this.tokenName = tokenName;
+  }
+
+  public String getTokenId() {
+    return tokenId;
+  }
+
+  public void setTokenId(String tokenId) {
+    this.tokenId = tokenId;
+  }
+
+  public String getHashedToken() {
+    return hashedToken;
+  }
+
+  public void setHashedToken(String hashedToken) {
+    this.hashedToken = hashedToken;
+  }
+}
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java
index 1cc535f..79b8349 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java
@@ -19,16 +19,21 @@
 package org.apache.streampipes.model.client.user;
 
 import com.google.gson.annotations.SerializedName;
+import org.apache.streampipes.model.shared.annotation.TsModel;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
+@TsModel
 public class User {
 
 	private @SerializedName("_id") String userId;
 	protected @SerializedName("_rev") String rev;
 	protected String email;
+
+	private String username;
+	private String fullName;
 	private String password;
 	
 	private List<Element> ownSources;
@@ -39,12 +44,15 @@ public class User {
 	private List<String> preferredSepas;
 	private List<String> preferredActions;
 
+	private List<UserApiToken> userApiTokens;
+
 	private boolean hideTutorial;
 
 	private Set<Role> roles;	
 
 	public User() {
 		this.hideTutorial = false;
+		this.userApiTokens = new ArrayList<>();
 	}
 	
 	public User(String email, String password, Set<Role> roles, List<Element> ownSources, List<Element> ownSepas, List<Element> ownActions) {
@@ -202,6 +210,30 @@ public class User {
 		this.preferredActions.remove(elementId);
 	}
 
+	public String getUsername() {
+		return username;
+	}
+
+	public void setUsername(String username) {
+		this.username = username;
+	}
+
+	public String getFullName() {
+		return fullName;
+	}
+
+	public void setFullName(String fullName) {
+		this.fullName = fullName;
+	}
+
+	public List<UserApiToken> getUserApiTokens() {
+		return userApiTokens;
+	}
+
+	public void setUserApiTokens(List<UserApiToken> userApiTokens) {
+		this.userApiTokens = userApiTokens;
+	}
+
 	private Element find(String elementId, List<Element> source)
 	{
 		return source.stream().filter(f -> f.getElementId().equals(elementId)).findFirst().orElseThrow(IllegalArgumentException::new);
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserApiToken.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserApiToken.java
new file mode 100644
index 0000000..ee0ce0a
--- /dev/null
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserApiToken.java
@@ -0,0 +1,62 @@
+/*
+ * 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.model.client.user;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class UserApiToken {
+
+  private String tokenId;
+  private String tokenName;
+
+  @JsonIgnore
+  private String hashedToken;
+
+  public UserApiToken() {
+  }
+
+  public UserApiToken(String tokenId, String tokenName, String hashedToken) {
+    this.tokenId = tokenId;
+    this.tokenName = tokenName;
+    this.hashedToken = hashedToken;
+  }
+
+  public String getTokenName() {
+    return tokenName;
+  }
+
+  public void setTokenName(String tokenName) {
+    this.tokenName = tokenName;
+  }
+
+  public String getHashedToken() {
+    return hashedToken;
+  }
+
+  public void setHashedToken(String hashedToken) {
+    this.hashedToken = hashedToken;
+  }
+
+  public String getTokenId() {
+    return tokenId;
+  }
+
+  public void setTokenId(String tokenId) {
+    this.tokenId = tokenId;
+  }
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java
index 4498e21..0cbc466 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java
@@ -18,11 +18,15 @@
 
 package org.apache.streampipes.rest.impl;
 
+import org.apache.streampipes.model.client.user.RawUserApiToken;
 import org.apache.streampipes.model.message.Notifications;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
+import org.apache.streampipes.user.management.service.TokenService;
 
 import javax.ws.rs.*;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.util.stream.Collectors;
 
 @Path("/v2/users/{email}")
 public class User extends AbstractRestInterface {
@@ -47,6 +51,15 @@ public class User extends AbstractRestInterface {
         if (user != null) {
             org.apache.streampipes.model.client.user.User existingUser = getUser(user.getEmail());
             user.setPassword(existingUser.getPassword());
+            user.setUserApiTokens(existingUser
+                    .getUserApiTokens()
+                    .stream()
+                    .filter(existingToken -> user.getUserApiTokens()
+                            .stream()
+                            .anyMatch(updatedToken -> existingToken
+                                    .getTokenId()
+                                    .equals(updatedToken.getTokenId())))
+                    .collect(Collectors.toList()));
             user.setRev(existingUser.getRev());
             getUserStorage().updateUser(user);
             return ok(Notifications.success("User updated"));
@@ -58,4 +71,14 @@ public class User extends AbstractRestInterface {
     private org.apache.streampipes.model.client.user.User getUser(String email) {
         return getUserStorage().getUser(email);
     }
+
+    @POST
+    @Path("tokens")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @JacksonSerialized
+    public Response createNewApiToken(@PathParam("email") String email, RawUserApiToken rawToken) {
+        RawUserApiToken generatedToken = new TokenService().createAndStoreNewToken(email, rawToken);
+        return ok(generatedToken);
+    }
 }
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
index 503d7c9..e560b6e 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
@@ -18,16 +18,16 @@
 
 package org.apache.streampipes.storage.couchdb.impl;
 
-import org.lightcouch.CouchDbClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.apache.streampipes.model.client.user.User;
 import org.apache.streampipes.storage.api.IUserStorage;
 import org.apache.streampipes.storage.couchdb.dao.AbstractDao;
 import org.apache.streampipes.storage.couchdb.utils.Utils;
+import org.lightcouch.CouchDbClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * User Storage.
@@ -42,12 +42,12 @@ public class UserStorage extends AbstractDao<User> implements IUserStorage {
     public UserStorage() {
         super(Utils::getCouchDbUserClient, User.class);
     }
-    
+
     @Override
     public List<User> getAllUsers()
     {
-        List<User> users = findAll();
-    	return users.stream().collect(Collectors.toList());
+      List<User> users = findAll();
+    	return new ArrayList<>(users);
     }
 
     @Override
@@ -70,7 +70,7 @@ public class UserStorage extends AbstractDao<User> implements IUserStorage {
     public void updateUser(User user) {
         update(user);
     }
-    
+
     @Override
     public boolean emailExists(String email)
     {
diff --git a/streampipes-user-management/pom.xml b/streampipes-user-management/pom.xml
index 1d5006f..3708843 100644
--- a/streampipes-user-management/pom.xml
+++ b/streampipes-user-management/pom.xml
@@ -36,7 +36,7 @@
         </dependency>
         <dependency>
             <groupId>org.apache.streampipes</groupId>
-            <artifactId>streampipes-storage-couchdb</artifactId>
+            <artifactId>streampipes-storage-management</artifactId>
             <version>0.68.0-SNAPSHOT</version>
         </dependency>
 
@@ -49,6 +49,13 @@
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-web</artifactId>
         </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
-</project>
\ No newline at end of file
+</project>
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java
index 6cf1316..6bcd452 100644
--- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java
@@ -19,17 +19,16 @@
 package org.apache.streampipes.user.management.authentication;
 
 
-import org.apache.shiro.authc.AuthenticationException;
-import org.apache.shiro.authc.AuthenticationInfo;
-import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.authc.SimpleAuthenticationInfo;
-import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authc.*;
 import org.apache.shiro.realm.Realm;
 import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.apache.streampipes.model.client.user.User;
+import org.apache.streampipes.user.management.service.TokenService;
+import org.apache.streampipes.user.management.service.UserService;
+import org.apache.streampipes.user.management.util.TokenUtil;
 import org.lightcouch.CouchDbException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.apache.streampipes.user.management.service.UserService;
 
 
 /**
@@ -57,7 +56,7 @@ public class StreamPipesRealm implements Realm {
    * So far we only support UsernamePasswordToken.
    */
   public boolean supports(AuthenticationToken authenticationToken) {
-    return authenticationToken instanceof UsernamePasswordToken;
+    return authenticationToken instanceof UsernamePasswordToken || authenticationToken instanceof BearerToken;
   }
 
   @Override
@@ -72,12 +71,7 @@ public class StreamPipesRealm implements Realm {
         String email = ((UsernamePasswordToken) authenticationToken).getUsername();
         UserService userService = new UserService(email);
 
-        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
-        SimplePrincipalCollection principals = new SimplePrincipalCollection();
-        principals.add(email, this.getName());
-
-        LOG.info(principals.toString());
-        info.setPrincipals(principals);
+        SimpleAuthenticationInfo info = makeInfo(email);
         info.setCredentials(userService.getPassword());
 
         if (credentialsMatcher.doCredentialsMatch(authenticationToken, info)) {
@@ -90,8 +84,26 @@ public class StreamPipesRealm implements Realm {
       } catch (CouchDbException | NullPointerException e) {
         e.printStackTrace();
       }
+    } else if (authenticationToken instanceof BearerToken) {
+      BearerToken token = (BearerToken) authenticationToken;
+      String hashedToken = TokenUtil.hashToken(token.getToken());
+      User user = new TokenService().findUserForToken(hashedToken);
+      SimpleAuthenticationInfo info = makeInfo(user.getEmail());
+
+      return info;
     }
 
     return null;
   }
-}
\ No newline at end of file
+
+  private SimpleAuthenticationInfo makeInfo(String email) {
+    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
+    SimplePrincipalCollection principals = new SimplePrincipalCollection();
+    principals.add(email, this.getName());
+
+    LOG.info(principals.toString());
+    info.setPrincipals(principals);
+
+    return info;
+  }
+}
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/service/TokenService.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/service/TokenService.java
new file mode 100644
index 0000000..76444e3
--- /dev/null
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/service/TokenService.java
@@ -0,0 +1,61 @@
+/*
+ * 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.user.management.service;
+
+import com.google.gson.JsonObject;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.streampipes.model.client.user.RawUserApiToken;
+import org.apache.streampipes.model.client.user.User;
+import org.apache.streampipes.storage.api.IUserStorage;
+import org.apache.streampipes.storage.couchdb.utils.Utils;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+import org.apache.streampipes.user.management.util.TokenUtil;
+import org.lightcouch.CouchDbClient;
+
+import java.util.List;
+
+public class TokenService {
+
+  public RawUserApiToken createAndStoreNewToken(String email, RawUserApiToken baseToken) {
+    User user = getUserStorage().getUser(email);
+    RawUserApiToken generatedToken = TokenUtil.createToken(baseToken.getTokenName());
+    storeToken(user, generatedToken);
+    generatedToken.setHashedToken("");
+    return generatedToken;
+  }
+
+  private void storeToken(User user, RawUserApiToken generatedToken) {
+    user.getUserApiTokens().add(TokenUtil.toUserToken(generatedToken));
+    getUserStorage().updateUser(user);
+  }
+
+  private IUserStorage getUserStorage() {
+    return StorageDispatcher.INSTANCE
+            .getNoSqlStore()
+            .getUserStorageAPI();
+  }
+
+  public User findUserForToken(String token) {
+    CouchDbClient dbClient = Utils.getCouchDbUserClient();
+    List<JsonObject> users = dbClient.view("users/token").key(token).includeDocs(true).query(JsonObject.class);
+    if (users.size() != 1) {
+      throw new AuthenticationException("None or too many users with matching token");
+    }
+    return getUserStorage().getUser(users.get(0).get("email").getAsString());
+  }
+}
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/TokenUtil.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/TokenUtil.java
new file mode 100644
index 0000000..e0c873f
--- /dev/null
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/TokenUtil.java
@@ -0,0 +1,62 @@
+/*
+ * 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.user.management.util;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.streampipes.model.client.user.RawUserApiToken;
+import org.apache.streampipes.model.client.user.UserApiToken;
+
+import java.util.UUID;
+
+public class TokenUtil {
+
+  private static final Integer TOKEN_LENGTH = 24;
+
+  public static RawUserApiToken createToken(String tokenName) {
+    RawUserApiToken rawToken = new RawUserApiToken();
+    rawToken.setTokenId(UUID.randomUUID().toString());
+    rawToken.setTokenName(tokenName);
+    rawToken.setRawToken(generateToken());
+    rawToken.setHashedToken(hashToken(rawToken.getRawToken()));
+
+    return rawToken;
+  }
+
+  public static UserApiToken toUserToken(RawUserApiToken rawToken) {
+    UserApiToken userApiToken = new UserApiToken();
+    userApiToken.setTokenId(rawToken.getTokenId());
+    userApiToken.setHashedToken(rawToken.getHashedToken());
+    userApiToken.setTokenName(rawToken.getTokenName());
+
+    return userApiToken;
+  }
+
+  public static boolean validateToken(String providedToken, String hashedToken) {
+    return hashToken(providedToken).equals(hashedToken);
+  }
+
+  private static String generateToken() {
+    return RandomStringUtils.randomAlphanumeric(TOKEN_LENGTH);
+  }
+
+  public static String hashToken(String token) {
+    return DigestUtils.sha256Hex(token);
+  }
+
+}
diff --git a/streampipes-user-management/src/test/java/org/apache/streampipes/user/management/util/TestTokenUtil.java b/streampipes-user-management/src/test/java/org/apache/streampipes/user/management/util/TestTokenUtil.java
new file mode 100644
index 0000000..38b8da0
--- /dev/null
+++ b/streampipes-user-management/src/test/java/org/apache/streampipes/user/management/util/TestTokenUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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.user.management.util;
+
+import org.apache.streampipes.model.client.user.RawUserApiToken;
+import org.apache.streampipes.model.client.user.UserApiToken;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class TestTokenUtil {
+
+  @Test
+  public void testCreateToken() {
+    String tokenName = "test-token";
+
+    RawUserApiToken rawToken = TokenUtil.createToken(tokenName);
+    assertEquals("test-token", rawToken.getTokenName());
+    assertNotNull(rawToken.getTokenId());
+    assertNotNull(rawToken.getRawToken());
+    assertNotNull(rawToken.getHashedToken());
+  }
+
+  @Test
+  public void testToUserToken() {
+    RawUserApiToken rawToken = TokenUtil.createToken("test-token");
+    UserApiToken token = TokenUtil.toUserToken(rawToken);
+
+    assertEquals(rawToken.getHashedToken(), token.getHashedToken());
+    assertEquals(rawToken.getTokenId(), token.getTokenId());
+    assertEquals(rawToken.getTokenName(), token.getTokenName());
+  }
+
+  @Test
+  public void testTokenValidation() {
+    RawUserApiToken rawToken = TokenUtil.createToken("test-token");
+    String rawApiKey = rawToken.getRawToken();
+
+    assertTrue(TokenUtil.validateToken(rawApiKey, rawToken.getHashedToken()));
+  }
+
+}
diff --git a/ui/deployment/app-routing.module.mst b/ui/deployment/app-routing.module.mst
index b76a04d..ec50e00 100644
--- a/ui/deployment/app-routing.module.mst
+++ b/ui/deployment/app-routing.module.mst
@@ -31,6 +31,7 @@ import {AlreadyConfiguredCanActivateGuard} from "./_guards/already-configured.ca
 import {LoggedInCanActivateGuard} from "./_guards/logged-in.can-activate.guard";
 import {InfoComponent} from "./info/info.component";
 import {NotificationsComponent} from "./notifications/notifications.component";
+import {ProfileComponent} from "./profile/profile.component";
 
 {{#modulesActive}}
 {{#ng5}}
@@ -53,7 +54,8 @@ const routes: Routes = [
       {{/modulesActive}}
       { path: 'notifications', component: NotificationsComponent },
       { path: 'info', component: InfoComponent },
-      { path: 'pipeline-details', component: PipelineDetailsComponent }
+      { path: 'pipeline-details', component: PipelineDetailsComponent },
+      { path: 'profile', component: ProfileComponent},
     ], canActivateChild: [AuthCanActivateChildrenGuard] }
 ];
 
@@ -67,4 +69,4 @@ const routes: Routes = [
       LoggedInCanActivateGuard
   ]
 })
-export class AppRoutingModule { }
\ No newline at end of file
+export class AppRoutingModule { }
diff --git a/ui/deployment/appng5.module.mst b/ui/deployment/appng5.module.mst
index 0d3494e..b1fdf9a 100644
--- a/ui/deployment/appng5.module.mst
+++ b/ui/deployment/appng5.module.mst
@@ -32,6 +32,7 @@ import { CoreModule } from './core/core.module';
 import { LoginModule } from './login/login.module';
 import { HomeModule } from './home/home.module';
 import { InfoModule } from './info/info.module';
+import {ProfileModule} from "./profile/profile.module";
 import { PipelineDetailsModule } from './pipeline-details/pipeline-details.module';
 import { NotificationCountService } from "./services/notification-count-service";
 import { AuthService } from "./services/auth.service";
@@ -80,6 +81,7 @@ import * as $ from 'jquery';
         PlatformServicesModule,
         PipelineDetailsModule,
         ServicesModule,
+        ProfileModule,
         {{#modulesActive}}
         {{#ng5}}
         {{{ng5_moduleName}}},
@@ -102,4 +104,4 @@ import * as $ from 'jquery';
 })
 export class AppModule {
 
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts
index f5f773a..ed25a92 100644
--- a/ui/src/app/app-routing.module.ts
+++ b/ui/src/app/app-routing.module.ts
@@ -31,6 +31,7 @@ import {AlreadyConfiguredCanActivateGuard} from "./_guards/already-configured.ca
 import {LoggedInCanActivateGuard} from "./_guards/logged-in.can-activate.guard";
 import {InfoComponent} from "./info/info.component";
 import {NotificationsComponent} from "./notifications/notifications.component";
+import {ProfileComponent} from "./profile/profile.component";
 
 import { EditorComponent } from './editor/editor.component';
 import { PipelinesComponent } from './pipelines/pipelines.component';
@@ -61,7 +62,8 @@ const routes: Routes = [
       { path: 'configuration', component: ConfigurationComponent },
       { path: 'notifications', component: NotificationsComponent },
       { path: 'info', component: InfoComponent },
-      { path: 'pipeline-details', component: PipelineDetailsComponent }
+      { path: 'pipeline-details', component: PipelineDetailsComponent },
+      { path: 'profile', component: ProfileComponent},
     ], canActivateChild: [AuthCanActivateChildrenGuard] }
 ];
 
@@ -75,4 +77,4 @@ const routes: Routes = [
       LoggedInCanActivateGuard
   ]
 })
-export class AppRoutingModule { }
\ No newline at end of file
+export class AppRoutingModule { }
diff --git a/ui/src/app/core-model/gen/streampipes-model-client.ts b/ui/src/app/core-model/gen/streampipes-model-client.ts
index e9fdac0..b168ade 100644
--- a/ui/src/app/core-model/gen/streampipes-model-client.ts
+++ b/ui/src/app/core-model/gen/streampipes-model-client.ts
@@ -19,7 +19,22 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.24.612 on 2020-09-14 21:40:09.
+// Generated using typescript-generator version 2.27.744 on 2021-01-05 12:29:12.
+
+export class Element {
+    elementId: string;
+    publicElement: boolean;
+
+    static fromData(data: Element, target?: Element): Element {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new Element();
+        instance.elementId = data.elementId;
+        instance.publicElement = data.publicElement;
+        return instance;
+    }
+}
 
 export class FileMetadata {
     createdAt: number;
@@ -71,6 +86,83 @@ export class MatchingResultMessage {
     }
 }
 
+export class RawUserApiToken {
+    hashedToken: string;
+    rawToken: string;
+    tokenId: string;
+    tokenName: string;
+
+    static fromData(data: RawUserApiToken, target?: RawUserApiToken): RawUserApiToken {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new RawUserApiToken();
+        instance.rawToken = data.rawToken;
+        instance.hashedToken = data.hashedToken;
+        instance.tokenName = data.tokenName;
+        instance.tokenId = data.tokenId;
+        return instance;
+    }
+}
+
+export class User {
+    email: string;
+    fullName: string;
+    hideTutorial: boolean;
+    ownActions: Element[];
+    ownSepas: Element[];
+    ownSources: Element[];
+    password: string;
+    preferredActions: string[];
+    preferredSepas: string[];
+    preferredSources: string[];
+    rev: string;
+    roles: Role[];
+    userApiTokens: UserApiToken[];
+    userId: string;
+    username: string;
+
+    static fromData(data: User, target?: User): User {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new User();
+        instance.userId = data.userId;
+        instance.rev = data.rev;
+        instance.email = data.email;
+        instance.username = data.username;
+        instance.fullName = data.fullName;
+        instance.password = data.password;
+        instance.ownSources = __getCopyArrayFn(Element.fromData)(data.ownSources);
+        instance.ownSepas = __getCopyArrayFn(Element.fromData)(data.ownSepas);
+        instance.ownActions = __getCopyArrayFn(Element.fromData)(data.ownActions);
+        instance.preferredSources = __getCopyArrayFn(__identity<string>())(data.preferredSources);
+        instance.preferredSepas = __getCopyArrayFn(__identity<string>())(data.preferredSepas);
+        instance.preferredActions = __getCopyArrayFn(__identity<string>())(data.preferredActions);
+        instance.userApiTokens = __getCopyArrayFn(UserApiToken.fromData)(data.userApiTokens);
+        instance.hideTutorial = data.hideTutorial;
+        instance.roles = __getCopyArrayFn(__identity<Role>())(data.roles);
+        return instance;
+    }
+}
+
+export class UserApiToken {
+    tokenId: string;
+    tokenName: string;
+
+    static fromData(data: UserApiToken, target?: UserApiToken): UserApiToken {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new UserApiToken();
+        instance.tokenId = data.tokenId;
+        instance.tokenName = data.tokenName;
+        return instance;
+    }
+}
+
+export type Role = "SYSTEM_ADMINISTRATOR" | "MANAGER" | "OPERATOR" | "DIMENSION_OPERATOR" | "USER_DEMO" | "BUSINESS_ANALYST";
+
 function __getCopyArrayFn<T>(itemCopyFn: (item: T) => T): (array: T[]) => T[] {
     return (array: T[]) => __copyArray(array, itemCopyFn);
 }
diff --git a/ui/src/app/core-model/gen/streampipes-model.ts b/ui/src/app/core-model/gen/streampipes-model.ts
index 5af919b..716c1d7 100644
--- a/ui/src/app/core-model/gen/streampipes-model.ts
+++ b/ui/src/app/core-model/gen/streampipes-model.ts
@@ -1,3 +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.
+ *
+ */
+
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.html b/ui/src/app/core/components/toolbar/toolbar.component.html
index c531d89..991b82c 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.html
+++ b/ui/src/app/core/components/toolbar/toolbar.component.html
@@ -60,6 +60,14 @@
                 </div>
             </div>
             <mat-menu #menu="matMenu" id="account">
+                <div class="current-user">
+                    {{userEmail}}
+                </div>
+                <mat-divider></mat-divider>
+                <button mat-menu-item (click)="openProfile()" style="min-width:0px;">
+                    <mat-icon aria-label="Info">perm_identity</mat-icon>
+                    Profile
+                </button>
                 <button mat-menu-item (click)="openInfo()" style="min-width:0px;">
                     <mat-icon aria-label="Info">help</mat-icon>
                     Info
@@ -69,7 +77,7 @@
                     <mat-icon aria-label="Documentation">description</mat-icon>
                     Documentation
                 </button>
-                <mat-menu-divider></mat-menu-divider>
+                <mat-divider></mat-divider>
                 <button mat-menu-item id="sp_logout" (click)="logout()"
                            style="min-width:0px;">
                     <mat-icon aria-label="Logout">exit_to_app</mat-icon>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/core/components/toolbar/toolbar.component.scss
index 61efca3..ce59853 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/core/components/toolbar/toolbar.component.scss
@@ -19,4 +19,15 @@
 
 ::ng-deep .cdk-overlay-pane .feedback-menu-content{
   min-width:500px;
-}
\ No newline at end of file
+}
+
+.current-user {
+  display: block;
+  line-height: 48px;
+  height: 48px;
+  padding: 0 16px;
+  text-align: left;
+  text-decoration: none;
+  max-width: 100%;
+  position: relative;
+}
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.ts b/ui/src/app/core/components/toolbar/toolbar.component.ts
index 7d84414..b4b77b0 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.ts
+++ b/ui/src/app/core/components/toolbar/toolbar.component.ts
@@ -35,6 +35,7 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
   @ViewChild('accountMenuOpen') accountMenuOpen: MatMenuTrigger;
 
   versionInfo: VersionInfo;
+  userEmail;
 
   constructor(Router: Router,
               private RestApi: RestApi,
@@ -43,8 +44,9 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
   }
 
   ngOnInit(): void {
+    this.userEmail = this.AuthStatusService.email;
     super.onInit();
-    this.getVersion()
+    this.getVersion();
   }
 
   closeFeedbackWindow() {
@@ -61,6 +63,11 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
     this.activePage = "Info";
   }
 
+  openProfile() {
+    this.Router.navigate(["profile"]);
+    this.activePage = "Profile";
+  }
+
   logout() {
     this.RestApi.logout().subscribe(() => {
       this.AuthStatusService.user = undefined;
@@ -74,4 +81,4 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
       this.versionInfo = response as VersionInfo;
     })
   }
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/profile/components/basic-profile-settings.ts b/ui/src/app/profile/components/basic-profile-settings.ts
new file mode 100644
index 0000000..0f31f7d
--- /dev/null
+++ b/ui/src/app/profile/components/basic-profile-settings.ts
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ *
+ */
+
+import {ProfileService} from "../profile.service";
+import {User} from "../../core-model/gen/streampipes-model-client";
+import {Directive} from "@angular/core";
+
+@Directive()
+export abstract class BasicProfileSettings {
+
+  userData: User;
+  profileLoaded: boolean = false;
+  profileUpdating: boolean = false;
+  errorMessage: string;
+
+  constructor(protected profileService: ProfileService) {
+
+  }
+
+  receiveUserData() {
+    this.profileService
+        .getUserProfile()
+        .subscribe(userData => {
+          this.userData = userData;
+          this.onUserDataReceived();
+          this.profileLoaded = true;
+        });
+  }
+
+  saveProfileSettings() {
+    this.profileUpdating = true;
+    this.profileService.updateUserProfile(this.userData).subscribe(response => {
+      this.profileUpdating = false;
+      if (response.success) {
+        this.receiveUserData();
+      } else {
+        this.errorMessage = response.notifications[0].title;
+      }
+    });
+  }
+
+  abstract onUserDataReceived();
+}
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
new file mode 100644
index 0000000..dc7bdb9
--- /dev/null
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.html
@@ -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.
+  ~
+  -->
+
+<div fxLayout="row" class="page-container-padding">
+    <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" *ngIf="profileLoaded">
+        <div fxLayout="row" fxFlex="100">
+            <div fxFlex="300px" fxLayout="column" class="profile-section">
+                <div class="settings-title">Main Settings</div>
+                <div class="settings-description">Manage your basic profile settings here.</div>
+            </div>
+            <div fxFill fxLayout="column">
+                <mat-form-field fxFlex>
+                    <mat-label>Email</mat-label>
+                    <input [(ngModel)]="userData.email" matInput type="email" name="email" disabled/>
+                    <mat-hint>Your mail address can't be changed currently.</mat-hint>
+                </mat-form-field>
+                <mat-form-field fxFlex>
+                    <mat-label>Username</mat-label>
+                    <input [(ngModel)]="userData.username" matInput/>
+                </mat-form-field>
+                <mat-form-field fxFlex>
+                    <mat-label>Full Name</mat-label>
+                    <input [(ngModel)]="userData.fullName" matInput/>
+                </mat-form-field>
+                <div>
+                    <button mat-button mat-raised-button color="primary"
+                        (click)="saveProfileSettings()">
+                        Update profile
+                    </button>
+                </div>
+            </div>
+        </div>
+        <mat-divider></mat-divider>
+    </div>
+
+</div>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/components/general/general-profile-settings.component.scss
similarity index 78%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/components/general/general-profile-settings.component.scss
index 61efca3..965fae7 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.scss
@@ -16,7 +16,20 @@
  *
  */
 
+.settings-title {
+  font-weight: bold;
+  font-size: 16pt;
+}
 
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
-  min-width:500px;
-}
\ No newline at end of file
+.settings-description {
+  font-size: 12pt;
+}
+
+.profile-section {
+  border-right: 2px solid gray;
+  margin-right: 15px;
+}
+
+.title-section {
+  width: 300px;
+}
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/components/general/general-profile-settings.component.ts
similarity index 54%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/components/general/general-profile-settings.component.ts
index 61efca3..bd0726b 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.ts
@@ -16,7 +16,27 @@
  *
  */
 
+import {Component, OnInit} from "@angular/core";
+import {ProfileService} from "../../profile.service";
+import {User} from "../../../core-model/gen/streampipes-model-client";
+import {BasicProfileSettings} from "../basic-profile-settings";
 
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
-  min-width:500px;
-}
\ No newline at end of file
+@Component({
+  selector: 'general-profile-settings',
+  templateUrl: './general-profile-settings.component.html',
+  styleUrls: ['./general-profile-settings.component.scss']
+})
+export class GeneralProfileSettingsComponent extends BasicProfileSettings implements OnInit {
+
+  constructor(profileService: ProfileService) {
+    super(profileService);
+  }
+
+  ngOnInit(): void {
+    this.receiveUserData();
+  }
+
+  onUserDataReceived() {
+  }
+
+}
diff --git a/ui/src/app/profile/components/token/token-management-settings.component.html b/ui/src/app/profile/components/token/token-management-settings.component.html
new file mode 100644
index 0000000..724e2dc
--- /dev/null
+++ b/ui/src/app/profile/components/token/token-management-settings.component.html
@@ -0,0 +1,90 @@
+<!--
+  ~ 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" class="page-container-padding">
+    <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" *ngIf="profileLoaded">
+        <div fxLayout="row" fxFlex="100">
+            <div fxFlex="300px" fxLayout="column" class="profile-section">
+                <div class="settings-title">API Keys</div>
+                <div class="settings-description">Manage your API keys for third-party application access to
+                    StreamPipes.
+                </div>
+            </div>
+            <div fxFill fxLayout="column">
+                <div fxLayout="column" class="subsection">
+                    <div class="subsection-title">New API key</div>
+                    <mat-form-field fxFlex>
+                        <mat-label>Name</mat-label>
+                        <input [(ngModel)]="newTokenName" matInput/>
+                    </mat-form-field>
+                    <div>
+                        <button mat-button mat-raised-button color="primary"
+                                (click)="requestNewKey()">
+                            Create new API key
+                        </button>
+                    </div>
+                </div>
+                <div fxLayout="column" class="subsection mt-10" *ngIf="newTokenCreated">
+                    <div class="subsection-title">Key created</div>
+                    <div class="subsection-small">Your new API key has been created. Please copy the key now - you won't be able to see the key again.</div>
+                    <div fxFlex="100" fxLayout="row" fxLayoutAlign="start center" class="new-token">
+                        <div class="token-name">{{newlyCreatedToken.tokenName}}</div>
+                        <div class="displayed-token">
+                            {{newlyCreatedToken.rawToken}}
+                        </div>
+                        <button mat-button mat-raised-button color="primary"
+                                [cdkCopyToClipboard]="newlyCreatedToken.rawToken">
+                            Copy
+                        </button>
+                    </div>
+                </div>
+                <mat-divider class="divider"></mat-divider>
+                <div fxLayout="column" class="subsection mt-10">
+                    <div class="subsection-title">Existing API keys</div>
+                    <div *ngIf="userData.userApiTokens.length == 0">(no keys available)</div>
+                    <table mat-table [dataSource]="apiKeyDataSource" class="mat-elevation-z0" *ngIf="userData.userApiTokens.length > 0">
+                        <ng-container matColumnDef="name">
+                            <th mat-header-cell *matHeaderCellDef> Name</th>
+                            <td mat-cell *matCellDef="let element"> {{element.tokenName}} </td>
+                        </ng-container>
+
+                        <!-- Name Column -->
+                        <ng-container matColumnDef="action">
+                            <th mat-header-cell *matHeaderCellDef> Action</th>
+                            <td mat-cell *matCellDef="let element">
+                                <div fxLayout="end end">
+                                <button mat-button mat-raised-button color="warn"
+                                        (click)="revokeApiKey(element)">
+                                    Revoke
+                                </button>
+                                </div>
+                            </td>
+                        </ng-container>
+
+                        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+                        <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+                    </table>
+                </div>
+            </div>
+            <div fxFill fxLayout="column">
+            </div>
+        </div>
+        <mat-divider></mat-divider>
+    </div>
+
+</div>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/components/token/token-management-settings.component.scss
similarity index 52%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/components/token/token-management-settings.component.scss
index 61efca3..4264dcf 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/components/token/token-management-settings.component.scss
@@ -16,7 +16,60 @@
  *
  */
 
+.settings-title {
+  font-weight: bold;
+  font-size: 16pt;
+}
 
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
-  min-width:500px;
-}
\ No newline at end of file
+.settings-description {
+  font-size: 12pt;
+}
+
+.profile-section {
+  border-right: 2px solid gray;
+  margin-right: 15px;
+}
+
+.title-section {
+  width: 300px;
+}
+
+.subsection-title {
+  font-size: 12pt;
+  font-weight: bold;
+  margin-bottom: 10px;
+}
+
+.subsection {
+  margin-bottom: 15px;
+}
+
+.mt-10 {
+  margin-top: 10px;
+}
+
+.divider {
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+
+.new-token {
+
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+.token-name {
+  margin-right: 10px;
+  font-weight: bold;
+  padding: 10px;
+}
+
+.displayed-token {
+  background: #f6f6f6;
+  border: 1px solid #cccccc;
+  padding: 10px;
+  color: black;
+  margin-left: 10px;
+  margin-right: 10px;
+}
diff --git a/ui/src/app/profile/components/token/token-management-settings.component.ts b/ui/src/app/profile/components/token/token-management-settings.component.ts
new file mode 100644
index 0000000..dfced55
--- /dev/null
+++ b/ui/src/app/profile/components/token/token-management-settings.component.ts
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ *
+ */
+
+import {Component, OnInit} from "@angular/core";
+import {BasicProfileSettings} from "../basic-profile-settings";
+import {RawUserApiToken, UserApiToken} from "../../../core-model/gen/streampipes-model-client";
+import {MatTableDataSource} from "@angular/material/table";
+
+@Component({
+  selector: 'token-management-settings',
+  templateUrl: './token-management-settings.component.html',
+  styleUrls: ['./token-management-settings.component.scss']
+})
+export class TokenManagementSettingsComponent extends BasicProfileSettings implements OnInit {
+
+  newTokenName: string;
+  newTokenCreated: boolean = false;
+  newlyCreatedToken: RawUserApiToken;
+
+  displayedColumns: string[] = ['name', 'action'];
+  apiKeyDataSource: MatTableDataSource<UserApiToken>;
+
+  ngOnInit(): void {
+    this.receiveUserData();
+  }
+
+  requestNewKey() {
+    let baseToken: RawUserApiToken = this.makeBaseToken();
+    this.profileService.requestNewApiToken(baseToken).subscribe(result => {
+      this.newlyCreatedToken = result;
+      this.newTokenCreated = true;
+      this.newTokenName = "";
+      this.receiveUserData();
+    });
+  }
+
+  makeBaseToken(): RawUserApiToken {
+    let baseToken = new RawUserApiToken();
+    baseToken.tokenName = this.newTokenName;
+    return baseToken;
+  }
+
+  revokeApiKey(apiKey: UserApiToken) {
+    var removeIndex = this.userData.userApiTokens.map(token => token.tokenId).indexOf(apiKey.tokenId);
+    this.userData.userApiTokens.splice(removeIndex, 1);
+    this.profileService.updateUserProfile(this.userData).subscribe(response => {
+      this.receiveUserData();
+    })
+  }
+
+  onUserDataReceived() {
+    this.apiKeyDataSource = new MatTableDataSource<UserApiToken>(this.userData.userApiTokens);
+  }
+
+}
diff --git a/ui/src/app/profile/profile.component.html b/ui/src/app/profile/profile.component.html
new file mode 100644
index 0000000..329d064
--- /dev/null
+++ b/ui/src/app/profile/profile.component.html
@@ -0,0 +1,42 @@
+<!--
+  ~ 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="column" class="page-container">
+    <div fxLayout="row" class="border" style="padding:0px;background-color:#f6f6f6;">
+        <div fxFlex="100" style="line-height:24px;border-bottom:1px solid #ccc">
+            <div fxFlex="100" fxLayout="row">
+                <div fxFlex fxLayoutAlign="start center" [attr.id]="'peType'">
+                    <mat-tab-group [selectedIndex]="selectedIndex" (selectedIndexChange)="selectedIndexChange($event)">
+                        <mat-tab label="General Settings"></mat-tab>
+                        <mat-tab label="API Keys"></mat-tab>
+                    </mat-tab-group>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="fixed-height page-container-padding-inner" fxLayout="column" fxFlex="100">
+        <div class="fixed-height page-container-padding-inner" fxLayout="column" fxFlex="100" *ngIf="selectedIndex == 0">
+            <general-profile-settings fxFlex="100"></general-profile-settings>
+        </div>
+        <div class="fixed-height page-container-padding-inner" fxLayout="column" fxFlex="100" *ngIf="selectedIndex == 1">
+            <token-management-settings fxFlex="100"></token-management-settings>
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/profile.component.scss
similarity index 91%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/profile.component.scss
index 61efca3..d727140 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/profile.component.scss
@@ -16,7 +16,6 @@
  *
  */
 
-
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
-  min-width:500px;
-}
\ No newline at end of file
+.page-container-padding-inner {
+  padding: 10px;
+}
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/profile.component.ts
similarity index 69%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/profile.component.ts
index 61efca3..0ee7c0f 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/profile.component.ts
@@ -16,7 +16,22 @@
  *
  */
 
+import {Component, OnInit} from "@angular/core";
 
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
-  min-width:500px;
-}
\ No newline at end of file
+@Component({
+  selector: 'profile',
+  templateUrl: './profile.component.html',
+  styleUrls: ['./profile.component.scss']
+})
+export class ProfileComponent implements OnInit {
+
+  selectedIndex: number = 0;
+
+  ngOnInit(): void {
+  }
+
+  selectedIndexChange(index: number) {
+    this.selectedIndex = index;
+  }
+
+}
diff --git a/ui/src/app/profile/profile.module.ts b/ui/src/app/profile/profile.module.ts
new file mode 100644
index 0000000..f6242d5
--- /dev/null
+++ b/ui/src/app/profile/profile.module.ts
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ *
+ */
+
+import {NgModule} from "@angular/core";
+import {FlexLayoutModule} from "@angular/flex-layout";
+import {FormsModule} from "@angular/forms";
+import {MatTabsModule} from "@angular/material/tabs";
+import {MatButtonModule} from "@angular/material/button";
+import {CustomMaterialModule} from "../CustomMaterial/custom-material.module";
+import {CommonModule} from "@angular/common";
+import {ProfileComponent} from "./profile.component";
+import {TokenManagementSettingsComponent} from "./components/token/token-management-settings.component";
+import {GeneralProfileSettingsComponent} from "./components/general/general-profile-settings.component";
+import {ProfileService} from "./profile.service";
+import {MatDividerModule} from "@angular/material/divider";
+
+@NgModule({
+  imports: [
+    FlexLayoutModule,
+    FormsModule,
+    MatDividerModule,
+    MatTabsModule,
+    MatButtonModule,
+    CustomMaterialModule,
+    CommonModule,
+  ],
+  declarations: [
+    GeneralProfileSettingsComponent,
+    ProfileComponent,
+    TokenManagementSettingsComponent
+  ],
+  providers: [
+    ProfileService
+  ],
+  exports: [
+    ProfileComponent
+  ],
+  entryComponents: [
+    ProfileComponent
+  ]
+})
+export class ProfileModule {
+
+  constructor() {
+  }
+
+}
diff --git a/ui/src/app/profile/profile.service.ts b/ui/src/app/profile/profile.service.ts
new file mode 100644
index 0000000..89f5e5d
--- /dev/null
+++ b/ui/src/app/profile/profile.service.ts
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ *
+ */
+
+import {Injectable} from "@angular/core";
+import {PlatformServicesCommons} from "../platform-services/apis/commons.service";
+import {HttpClient} from "@angular/common/http";
+import {RawUserApiToken, User} from "../core-model/gen/streampipes-model-client";
+import {Observable} from "rxjs";
+import {map} from "rxjs/operators";
+import {Message} from "../core-model/gen/streampipes-model";
+
+@Injectable()
+export class ProfileService {
+
+  constructor(private http: HttpClient,
+              private platformServicesCommons: PlatformServicesCommons) {
+
+  }
+
+  getUserProfile(): Observable<User> {
+    return this.http.get(this.platformServicesCommons.authUserBasePath()).pipe(map(response => {
+      return User.fromData(response as any);
+    }));
+  }
+
+  updateUserProfile(userData: User): Observable<Message> {
+    return this.http.put(this.platformServicesCommons.authUserBasePath(), userData).pipe(map(response => {
+      return Message.fromData(response as any);
+    }))
+  }
+
+  requestNewApiToken(baseToken: RawUserApiToken): Observable<RawUserApiToken> {
+    return this.http.post(this.platformServicesCommons.authUserBasePath() + "/tokens", baseToken)
+        .pipe(map(response => {
+          return RawUserApiToken.fromData(response as any);
+        }))
+  }
+}