diff --git a/src/main/java/net/geant/nmaas/portal/api/security/CustomAccessTokenController.java b/src/main/java/net/geant/nmaas/portal/api/security/CustomAccessTokenController.java
new file mode 100644
index 0000000000000000000000000000000000000000..66839fd385cb64d9b4750081062a1813a348a2de
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/api/security/CustomAccessTokenController.java
@@ -0,0 +1,52 @@
+package net.geant.nmaas.portal.api.security;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import net.geant.nmaas.portal.exceptions.ObjectNotFoundException;
+import net.geant.nmaas.portal.persistent.entity.AccessToken;
+import net.geant.nmaas.portal.persistent.entity.User;
+import net.geant.nmaas.portal.service.CustomAccessTokenService;
+import net.geant.nmaas.portal.service.UserService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.security.Principal;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/tokens")
+@RequiredArgsConstructor
+@Log4j2
+public class CustomAccessTokenController {
+
+    private final CustomAccessTokenService accessTokenService;
+    private final UserService userService;
+
+    @GetMapping()
+    public List<AccessToken> getAll(Principal principal) {
+        User user = getUser(principal);
+        return accessTokenService.getAll(user.getId());
+    }
+
+    @PostMapping()
+    public AccessToken createNewToken(Principal principal, @RequestBody String name) {
+        User user = getUser(principal);
+        return accessTokenService.createToken(user.getId(), name);
+    }
+
+    @PutMapping("/{id}")
+    public void invalidateToken(@PathVariable Long id) {
+        accessTokenService.invalidate(id);
+    }
+
+    private User getUser(Principal principal) {
+        String principalName = principal.getName();
+        return userService.findByUsername(principalName)
+                .orElseThrow(() -> new ObjectNotFoundException("User not found"));
+    }
+}
diff --git a/src/main/java/net/geant/nmaas/portal/persistent/entity/AccessToken.java b/src/main/java/net/geant/nmaas/portal/persistent/entity/AccessToken.java
new file mode 100644
index 0000000000000000000000000000000000000000..80c117b2a178e8aee330990a737638c0fdc42960
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/persistent/entity/AccessToken.java
@@ -0,0 +1,33 @@
+package net.geant.nmaas.portal.persistent.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "access_tokens")
+@NoArgsConstructor
+@Getter
+@Setter
+@AllArgsConstructor
+public class AccessToken {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String name;
+
+    private Long userId;
+
+    private String tokenValue;
+
+    private boolean valid;
+}
diff --git a/src/main/java/net/geant/nmaas/portal/persistent/repositories/AccessTokenRepository.java b/src/main/java/net/geant/nmaas/portal/persistent/repositories/AccessTokenRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..7655ad0afff9c210f50cbf7c843ce22cf59fa7eb
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/persistent/repositories/AccessTokenRepository.java
@@ -0,0 +1,12 @@
+package net.geant.nmaas.portal.persistent.repositories;
+
+import net.geant.nmaas.portal.persistent.entity.AccessToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface AccessTokenRepository extends JpaRepository<AccessToken, Long> {
+    List<AccessToken> findAllByUserId(Long userId);
+}
diff --git a/src/main/java/net/geant/nmaas/portal/service/CustomAccessTokenService.java b/src/main/java/net/geant/nmaas/portal/service/CustomAccessTokenService.java
new file mode 100644
index 0000000000000000000000000000000000000000..96e59e1eb7a6960580be1e8a2d86095363fa7cf2
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/service/CustomAccessTokenService.java
@@ -0,0 +1,13 @@
+package net.geant.nmaas.portal.service;
+
+import net.geant.nmaas.portal.persistent.entity.AccessToken;
+
+import java.util.List;
+
+public interface CustomAccessTokenService {
+
+    void invalidate(Long id);
+    AccessToken createToken(Long userId, String name);
+    List<AccessToken> getAll(Long userId);
+
+}
diff --git a/src/main/java/net/geant/nmaas/portal/service/impl/CustomAccessTokenServiceImpl.java b/src/main/java/net/geant/nmaas/portal/service/impl/CustomAccessTokenServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..58bea7e30a7400dfc9ec715effd725f4f1cf8858
--- /dev/null
+++ b/src/main/java/net/geant/nmaas/portal/service/impl/CustomAccessTokenServiceImpl.java
@@ -0,0 +1,56 @@
+package net.geant.nmaas.portal.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import net.geant.nmaas.portal.exceptions.ObjectNotFoundException;
+import net.geant.nmaas.portal.persistent.entity.AccessToken;
+import net.geant.nmaas.portal.persistent.repositories.AccessTokenRepository;
+import net.geant.nmaas.portal.service.CustomAccessTokenService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class CustomAccessTokenServiceImpl implements CustomAccessTokenService {
+
+    private final AccessTokenRepository accessTokenRepository;
+
+    @Override
+    public void invalidate(Long id) {
+        AccessToken token = findToken(id);
+        token.setValid(false);
+        accessTokenRepository.save(token);
+    }
+
+    @Override
+    public AccessToken createToken(Long userId, String name) {
+        AccessToken token = createNewToken(userId, name);
+        return accessTokenRepository.save(token);
+    }
+
+    @Override
+    public List<AccessToken> getAll(Long userId) {
+        return accessTokenRepository.findAllByUserId(userId);
+    }
+
+    private AccessToken createNewToken(Long userId, String name) {
+        AccessToken token = new AccessToken();
+        token.setName(name);
+        token.setUserId(userId);
+        token.setTokenValue(generateToken());
+        token.setValid(true);
+        return token;
+        }
+
+    private String generateToken() {
+        // uuid is a placeholder for now
+        return UUID.randomUUID().toString();
+    }
+
+    private AccessToken findToken(Long id) {
+        return accessTokenRepository
+                .findById(id)
+                .orElseThrow(() -> new ObjectNotFoundException("Could not find access token with id: " + id));
+    }
+}
diff --git a/src/main/resources/db/migration/common/V1.7.0_20240920_1430__addedAccessTokens.sql b/src/main/resources/db/migration/common/V1.7.0_20240920_1430__addedAccessTokens.sql
new file mode 100644
index 0000000000000000000000000000000000000000..999454960500d4bfcdc76b137adea5ee0929f469
--- /dev/null
+++ b/src/main/resources/db/migration/common/V1.7.0_20240920_1430__addedAccessTokens.sql
@@ -0,0 +1,8 @@
+create table access_tokens (
+       id bigint generated by default as identity,
+        name varchar(255),
+        token_value varchar(255),
+        user_id bigint,
+        valid boolean not null,
+        primary key (id)
+ );
\ No newline at end of file
diff --git a/src/test/shell/data/i18n/de.json b/src/test/shell/data/i18n/de.json
index b52b0a6004b08b2b98a258cac21fe5180623a4e1..3f309f2c926862104bb0340f28259ea99d40545c 100644
--- a/src/test/shell/data/i18n/de.json
+++ b/src/test/shell/data/i18n/de.json
@@ -1155,6 +1155,30 @@
       }
     }
   },
+  "TOKENS": {
+    "HEADER": "Tokens",
+    "TABLE": {
+      "ID": "ID",
+      "NAME": "Name",
+      "VALUE": "Value",
+      "VALID": "Valid",
+      "ACTIONS": "Actions"
+    },
+    "MODAL": {
+      "HEADER": "Add new access token",
+      "NAME": "Name",
+      "BUTTON_ADD": "Submit",
+      "BUTTON_CANCEL": "Cancel",
+      "ERROR": {
+        "NAME_REQUIRED": "Name is required",
+        "NAME_MINLENGTH": "Name must have at least 1 character",
+        "NAME_MAXLENGTH": "Name cannot be longer than 16 characters"
+      }
+    },
+    "BUTTON_INVALIDATE": "Invalidate",
+    "NO_TOKENS": "No tokens",
+    "NEW_TOKEN": "Add new token"
+  },
   "JSON_EDIT": {
     "INVALID_JSON": "Invalid JSON format. Changes made to the content of the wizard will not be persisted."
   },
diff --git a/src/test/shell/data/i18n/en.json b/src/test/shell/data/i18n/en.json
index dbfdaf12bd1c046a4c86d54aed4ac8bf846f5e4d..93077028cf154bffb800cabf2c4f5fb69f59e8ad 100644
--- a/src/test/shell/data/i18n/en.json
+++ b/src/test/shell/data/i18n/en.json
@@ -1156,6 +1156,30 @@
       }
     }
   },
+  "TOKENS": {
+    "HEADER": "Tokens",
+    "TABLE": {
+      "ID": "ID",
+      "NAME": "Name",
+      "VALUE": "Value",
+      "VALID": "Valid",
+      "ACTIONS": "Actions"
+    },
+    "MODAL": {
+      "HEADER": "Add new access token",
+      "NAME": "Name",
+      "BUTTON_ADD": "Submit",
+      "BUTTON_CANCEL": "Cancel",
+      "ERROR": {
+        "NAME_REQUIRED": "Name is required",
+        "NAME_MINLENGTH": "Name must have at least 1 character",
+        "NAME_MAXLENGTH": "Name cannot be longer than 16 characters"
+      }
+    },
+    "BUTTON_INVALIDATE": "Invalidate",
+    "NO_TOKENS": "No tokens",
+    "NEW_TOKEN": "Add new token"
+  },
   "JSON_EDIT": {
     "INVALID_JSON": "Invalid JSON format. Changes made to the content of the wizard will not be persisted."
   },
diff --git a/src/test/shell/data/i18n/fr.json b/src/test/shell/data/i18n/fr.json
index 6cc4d288c04ae373748290b6163f06dec9e79b8d..2a7b0829c24d5b9040062136bf67d4990d553d79 100644
--- a/src/test/shell/data/i18n/fr.json
+++ b/src/test/shell/data/i18n/fr.json
@@ -1156,6 +1156,30 @@
       }
     }
   },
+  "TOKENS": {
+    "HEADER": "Tokens",
+    "TABLE": {
+      "ID": "ID",
+      "NAME": "Name",
+      "VALUE": "Value",
+      "VALID": "Valid",
+      "ACTIONS": "Actions"
+    },
+    "MODAL": {
+      "HEADER": "Add new access token",
+      "NAME": "Name",
+      "BUTTON_ADD": "Submit",
+      "BUTTON_CANCEL": "Cancel",
+      "ERROR": {
+        "NAME_REQUIRED": "Name is required",
+        "NAME_MINLENGTH": "Name must have at least 1 character",
+        "NAME_MAXLENGTH": "Name cannot be longer than 16 characters"
+      }
+    },
+    "BUTTON_INVALIDATE": "Invalidate",
+    "NO_TOKENS": "No tokens",
+    "NEW_TOKEN": "Add new token"
+  },
   "JSON_EDIT": {
     "INVALID_JSON": "Invalid JSON format. Changes made to the content of the wizard will not be persisted."
   },
diff --git a/src/test/shell/data/i18n/pl.json b/src/test/shell/data/i18n/pl.json
index f6d296d186ce8f8e1f12eb3b4cf4c43281176f6c..582f4fc7dbf733607010587d6212b4f59ee9262b 100644
--- a/src/test/shell/data/i18n/pl.json
+++ b/src/test/shell/data/i18n/pl.json
@@ -1156,6 +1156,30 @@
       }
     }
   },
+  "TOKENS": {
+    "HEADER": "Tokens",
+    "TABLE": {
+      "ID": "ID",
+      "NAME": "Name",
+      "VALUE": "Value",
+      "VALID": "Valid",
+      "ACTIONS": "Actions"
+    },
+    "MODAL": {
+      "HEADER": "Add new access token",
+      "NAME": "Name",
+      "BUTTON_ADD": "Submit",
+      "BUTTON_CANCEL": "Cancel",
+      "ERROR": {
+        "NAME_REQUIRED": "Name is required",
+        "NAME_MINLENGTH": "Name must have at least 1 character",
+        "NAME_MAXLENGTH": "Name cannot be longer than 16 characters"
+      }
+    },
+    "BUTTON_INVALIDATE": "Invalidate",
+    "NO_TOKENS": "No tokens",
+    "NEW_TOKEN": "Add new token"
+  },
   "JSON_EDIT": {
     "INVALID_JSON": "Niewłaściwy format JSON. Zmiany wprowadzone do zawartości formularza nie zostaną zapisane."
   },