diff --git a/soitool/coder.py b/soitool/coder.py
index 533e25995904ffd936170b028beb5071f8ce432d..fdc418a5ea286d50c1bb4b824b2aa275f177b145 100644
--- a/soitool/coder.py
+++ b/soitool/coder.py
@@ -1,11 +1,12 @@
-"""Generate codes."""
-import string
+"""Generate codes.
 
-# https://realpython.com/lessons/cryptographically-secure-random-data-python/
+Source: # https://realpython.com/lessons/cryptographically-secure-random-data-python/
+"""
+import string
 import secrets
 
 
-def get_code(code_length, mode="ascii"):
+def get_code(code_length, mode="ascii", space_interval=0):
     """
     Generate a single random code.
 
@@ -13,18 +14,18 @@ def get_code(code_length, mode="ascii"):
     ----------
     code_length : int
         The length of the code
-
     mode : string
         'ascii' for letters (default), 'digits' for digits and 'combo'
-        for combination of letters and digits.
+        for combination of letters and digits, by default 'ascii'.
+    space_interval : int or 0
+        Spaces will be inserted to code each interval if not 0, by default 0.
 
     Return
     ------
     code : string
-        The code
+        The code.
     """
     code = ""
-    i = 0
 
     if mode == "ascii":
         characters = string.ascii_uppercase
@@ -37,14 +38,26 @@ def get_code(code_length, mode="ascii"):
             "Invalid value for argument 'mode': " "'{}'".format(mode)
         )
 
+    if not isinstance(space_interval, int):
+        raise ValueError(
+            "Invalid value for argument 'separate_interval': "
+            "'{}'".format(space_interval)
+        )
+
+    i = 0
     while i < code_length:
         letter = secrets.choice(characters)
         code += letter
         i += 1
+
+    # Add spaces to code if interval is given
+    if space_interval > 0:
+        code = insert_spaces(code, space_interval)
+
     return code
 
 
-def get_code_set(count, code_length, mode="ascii"):
+def get_code_set(count, code_length, mode="ascii", space_interval=0):
     """
     Generate a set of unique, random codes.
 
@@ -52,13 +65,13 @@ def get_code_set(count, code_length, mode="ascii"):
     ----------
     count : int
         Number of codes to be returned
-
     code_length : int
         The length of each code
-
     mode : string
         'ascii' for letters (default), 'digits' for digits and 'combo'
         for combination of letters and digits.
+    space_interval : int or 0
+        Spaces will be inserted to code each interval if not 0, by default 0.
 
     Return
     ------
@@ -68,7 +81,8 @@ def get_code_set(count, code_length, mode="ascii"):
     codes = set()
 
     while len(codes) < count:
-        codes.add(get_code(code_length, mode))
+        code = get_code(code_length, mode, space_interval)
+        codes.add(code)
 
     return codes
 
@@ -93,3 +107,26 @@ def get_code_length_needed(number_of_entries):
         code_length = code_length + 1
 
     return code_length
+
+
+def insert_spaces(code, interval):
+    """Insert space after every x'th character, x = interval.
+
+    Parameters
+    ----------
+    code : string
+        String to add spaces to.
+    interval : int
+        Interval for inserting spaces.
+
+    Returns
+    -------
+    string
+        code separated with spaces.
+    """
+    # Convert to list to insert spaces between characters
+    code = list(code)
+    for i in range(interval - 1, len(code), interval):
+        code[i] += " "
+
+    return "".join(code)
diff --git a/soitool/media/authentificationboardmodule.PNG b/soitool/media/authentificationboardmodule.PNG
new file mode 100644
index 0000000000000000000000000000000000000000..439ad8b1928211b7a4c8341377123fb2d7dbd303
Binary files /dev/null and b/soitool/media/authentificationboardmodule.PNG differ
diff --git a/soitool/modules/module_authentication_board.py b/soitool/modules/module_authentication_board.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd0bfa2842c52dd3093ec51d52aa5d922139fc8b
--- /dev/null
+++ b/soitool/modules/module_authentication_board.py
@@ -0,0 +1,279 @@
+"""Module containing SOI-module 'Autentifiseringstavle'."""
+import string
+from PySide2.QtWidgets import QTableWidget, QTableWidgetItem
+from PySide2 import QtGui
+from PySide2.QtCore import Qt
+from soitool.coder import get_code_set, get_code
+from soitool.modules.module_base import (
+    ModuleBase,
+    get_table_size,
+    resize_table,
+    HEADLINE_FONT,
+)
+
+START_NO_OF_AUTHENTICATION_CODES = 10
+CODE_LENGTH = 25
+CODE_CHARACTERS = "ascii"  # Has to be 'ascii', 'digits' or 'combo'
+# Codes will consist of A-Z if 'ascii', 0-9 if 'digits' and A-Z+0-9 if 'combo'.
+ROW_IDENTIFIERS = string.ascii_uppercase  # Characters for first column,
+# it's length determines maximum number of codes (rows).
+SPACE_INTERVAL = 5  # Adds space between sets of characters, 0 => no spaces
+# If code is 123456 and interval is 2, code will be 12 34 56
+
+HEADLINE_TEXT = "Autentiseringstavle"
+
+
+class Meta(type(ModuleBase), type(QTableWidget)):
+    """Used as a metaclass to enable multiple inheritance."""
+
+
+class AuthenticationBoardModule(ModuleBase, QTableWidget, metaclass=Meta):
+    """Modified QTableWidget representing a 'Autentifiseringstavle'.
+
+    By default, the widget initializes with a headline, a row-count of
+    START_NO_OF_AUTHENTIFICATION_CODES and three columns.
+    Row x in the first column contains the character ROW_IDENTIFIERS[x].
+    Row x in the second column contains the character x.
+    Row x in the third column contains an authentification code.
+
+    If parameters are given, the widget initializes accordingly:
+    'size' is a dict: {"width": int, "height": int},
+    'data' is a 2D list where data[0] is the headline,
+    and data[x][y] represents row x, column y.
+
+    The widget does not use more room than needed, and resizes dynamically.
+    It has shortcuts for adding and removing rows.
+
+    Inherits from ModuleBase and QTableWidget.
+    ModuleBase is used as an interface, it's methods are overridden.
+    """
+
+    def __init__(self, size=None, data=None):
+        self.type = "AuthentificationBoardModule"
+        QTableWidget.__init__(self)
+        ModuleBase.__init__(self)
+
+        if CODE_CHARACTERS == "ascii":
+            self.code_characters = string.ascii_uppercase
+        elif CODE_CHARACTERS == "digits":
+            self.code_characters = string.digits
+        elif CODE_CHARACTERS == "combo":
+            self.code_characters = string.ascii_uppercase + string.digits
+        else:
+            raise ValueError(
+                "Invalid value for CONSTANT 'CODE_CHARACTERS': "
+                "'{}'".format(CODE_CHARACTERS)
+            )
+
+        # Remove headers and scrollbars
+        self.horizontalHeader().hide()
+        self.verticalHeader().hide()
+        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+
+        # If parameters are None, generate new table
+        if size is None and data is None:
+            self.generate_table()
+            self.resizeColumnsToContents()
+            self.insert_headline()
+
+            # Resize height of rows and set size of window
+            resize_table(self, resize_column=False)
+        else:
+            self.setColumnCount(len(data[1]))
+            self.setRowCount(len(data) - 1)  # - 1 to skip headline
+
+            # Set cell-items
+            for i in range(self.rowCount()):
+                for j in range(self.columnCount()):
+                    item = QTableWidgetItem(data[i + 1][j])  # +1 skip headline
+                    self.setItem(i, j, item)
+
+            self.resizeColumnsToContents()
+            resize_table(self, resize_column=False)
+            self.setFixedWidth(size["width"])
+            self.setFixedHeight(size["height"])
+
+            self.insert_headline(data[0])
+
+    def generate_table(self):
+        """Insert row identifiers and authentification codes."""
+        # Set number of rows and columns
+        self.setRowCount(START_NO_OF_AUTHENTICATION_CODES)
+        self.setColumnCount(3)
+
+        # Generate codes
+        codes = list(
+            get_code_set(
+                START_NO_OF_AUTHENTICATION_CODES,
+                CODE_LENGTH,
+                CODE_CHARACTERS,
+                SPACE_INTERVAL,
+            )
+        )
+
+        # Insert table data
+        for i in range(START_NO_OF_AUTHENTICATION_CODES):
+            # Insert non-editable row identifier in first column
+            item_first = QTableWidgetItem(ROW_IDENTIFIERS[i])
+            item_first.setFlags(item_first.flags() ^ Qt.ItemIsEditable)
+            self.setItem(i, 0, item_first)
+
+            # Insert non-editable row identifier (int) in second column
+            item_second = QTableWidgetItem(str(i))
+            item_second.setFlags(item_second.flags() ^ Qt.ItemIsEditable)
+            self.setItem(i, 1, item_second)
+
+            # Insert non-editable code in third column
+            item_third = QTableWidgetItem(codes[i])
+            item_third.setFlags(item_third.flags() ^ Qt.ItemIsEditable)
+            self.setItem(i, 2, item_third)
+
+    def insert_headline(self, text=HEADLINE_TEXT):
+        """Insert headline text.
+
+        Parameters
+        ----------
+        text : string, optional
+            The headline text, by default HEADLINE_TEXT
+        """
+        item_headline = QTableWidgetItem(text)
+        item_headline.setTextAlignment(Qt.AlignHCenter)
+        item_headline.setFont(HEADLINE_FONT)
+
+        self.insertRow(0)
+        self.setItem(0, 0, item_headline)
+        self.setSpan(0, 0, 1, self.columnCount())  # Make cell span all columns
+
+    def generate_unique_authentification_code(self):
+        """Generate authentification-code that does not already exist.
+
+        Returns
+        -------
+        string
+            Generated, unique authentification-code
+        """
+        # Get existing codes
+        existing_codes = self.get_codes()
+
+        # Randomly generate a new code until it is unique
+        unique_code = False
+        while not unique_code:
+            code = get_code(CODE_LENGTH, CODE_CHARACTERS, SPACE_INTERVAL)
+            unique_code = code not in existing_codes
+
+        return code
+
+    def keyPressEvent(self, event):
+        """Add or remove row when 'Ctrl + +' and 'Ctrl + -' are pressed."""
+        if (
+            event.modifiers() == Qt.ControlModifier
+            and event.key() == Qt.Key_Plus
+        ):
+            self.add_row()
+        elif (
+            event.modifiers() == Qt.ControlModifier
+            and event.key() == Qt.Key_Underscore
+        ):
+            self.remove_row()
+        else:
+            super(AuthenticationBoardModule, self).keyPressEvent(event)
+
+    def add_row(self):
+        """Insert row below the selected row and add data."""
+        row_index = self.currentRow()
+        # If maximum amount of rows not reached and a row is selected
+        # (+ 1 to skip row containing headline)
+        if self.rowCount() < len(ROW_IDENTIFIERS) + 1 and row_index != -1:
+            # Generate unique code and insert row
+            code = self.generate_unique_authentification_code()
+            self.insertRow(row_index + 1)
+
+            # Loop through all rows starting with the new row
+            for i in range(row_index + 1, self.rowCount()):
+                # Insert row identifier in first column
+                item_first = QTableWidgetItem(self.code_characters[i - 1])
+                item_first.setFlags(item_first.flags() ^ Qt.ItemIsEditable)
+                self.setItem(i, 0, item_first)
+
+                # Insert row identifier (int) in second column
+                item_second = QTableWidgetItem(str(i - 1))
+                item_second.setFlags(item_second.flags() ^ Qt.ItemIsEditable)
+                self.setItem(i, 1, item_second)
+
+            # Insert authentification-code in third column
+            item_third = QTableWidgetItem(code)
+            item_third.setFlags(item_third.flags() ^ Qt.ItemIsEditable)
+            self.setItem(row_index + 1, 2, item_third)
+
+            # Resize code-column in case it got wider
+            # Example: 'BGD' is wider than 'III' (depending on font)
+            self.resizeColumnToContents(2)
+            resize_table(self, resize_column=False)
+
+    def remove_row(self):
+        """Remove selected row."""
+        row_index = self.currentRow()
+        # If at least one row (+ headline-row) exists and a row other than
+        # headline-row is selected
+        if self.rowCount() > 2 and row_index != 0 and row_index != -1:
+            # Remove row
+            self.removeRow(row_index)
+            # 'Decrease' row identifiers below the removed row
+            # If first row is removed, identifier A,B,C becomes A,B (not B,C)
+            for i in range(row_index, self.rowCount()):
+                self.item(i, 0).setText(self.code_characters[i - 1])
+                self.item(i, 1).setText(str(i - 1))
+        resize_table(self, resize_column=False)
+
+    def get_codes(self):
+        """Get all authentification-codes in table.
+
+        Returns
+        -------
+        List
+            List containing authentification-codes.
+        """
+        codes = []
+
+        # Start with row 1 to skip headline-row
+        for i in range(1, self.rowCount()):
+            codes.append(self.item(i, 2).text())
+
+        return codes
+
+    def get_size(self):
+        """Return size of widget."""
+        return get_table_size(self)
+
+    def get_data(self):
+        """Return list containing all data.
+
+        Returns
+        -------
+        List
+            List[0] contains headline,
+            list[x][y] represents value of row x, column y.
+        """
+        content = []
+        item_headline = self.item(0, 0)
+        if item_headline is not None:
+            content.append(item_headline.text())
+
+        for i in range(1, self.rowCount()):
+            row = []
+            for j in range(self.columnCount()):
+                row.append(self.item(i, j).text())
+            content.append(row)
+
+        return content
+
+    @staticmethod
+    def get_user_friendly_name():
+        """Get user-friendly name of module."""
+        return "Autentiseringstavle"
+
+    @staticmethod
+    def get_icon():
+        """Get icon of module."""
+        return QtGui.QIcon("soitool/media/authentificationboardmodule.png")
diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py
index f252d1e7bdf8a95536e69700659665809a151b27..c608a17177d43a16371b675d8f5a815f82a87e27 100644
--- a/soitool/modules/module_base.py
+++ b/soitool/modules/module_base.py
@@ -1,5 +1,12 @@
 """Base/interface of each module."""
 from abc import ABC
+from PySide2 import QtGui
+
+# Font for module headline
+HEADLINE_FONT = QtGui.QFont()
+HEADLINE_FONT.setFamily("Arial")
+HEADLINE_FONT.setPointSize(12)
+HEADLINE_FONT.setWeight(100)
 
 
 class ModuleBase(ABC):
@@ -15,14 +22,6 @@ class ModuleBase(ABC):
         """Abstract method, should be implemented by derived class."""
         raise NotImplementedError
 
-    def set_pos(self, pos):
-        """Abstract method, should be implemented by derived class."""
-        raise NotImplementedError
-
-    def render_onto_pdf(self):
-        """Abstract method, should be implemented by derived class."""
-        raise NotImplementedError
-
     def get_data(self):
         """Abstract method, should be implemented by derived class."""
         raise NotImplementedError
@@ -36,3 +35,65 @@ class ModuleBase(ABC):
     def get_icon():
         """Abstract method, should be implemented by derived class."""
         raise NotImplementedError
+
+
+def resize_table(widget, resize_row=True, resize_column=True):
+    """Calculate and set the size of a QTableWidget.
+
+    Parameters
+    ----------
+    widget : QTableWidget
+        QTablewidget-instance to calculate and set size.
+    resize_row : bool
+        Resizes rows to contents if True, by default True.
+    resize_column : bool
+        Resizes columns to contents if True, by default True.
+    """
+    if resize_row:
+        widget.resizeRowsToContents()
+    if resize_column:
+        widget.resizeColumnsToContents()
+
+    width, height = get_table_size(widget)
+
+    widget.setFixedWidth(width)
+    widget.setFixedHeight(height)
+
+
+def get_table_size(widget):
+    """Calculate and return total width and height of a QTableWidget.
+
+    Parameters
+    ----------
+    widget : QTableWidget
+        QTableWidget-instance to calculate and return size of.
+
+    Returns
+    -------
+    Tuple
+        Total (width, height)
+    """
+    # Calculate total width and height of columns and rows
+    width = 0
+    height = 0
+
+    for i in range(widget.columnCount()):
+        width += widget.columnWidth(i) + 0.5
+
+    for i in range(widget.rowCount()):
+        height += widget.rowHeight(i) + 0.5
+
+    return width, height
+
+
+def set_module_pos(widget, pos):
+    """Set position of module (widget).
+
+    Parameters
+    ----------
+    widget : QWidget
+        Widget to move.
+    pos : QPoint
+        Position (x, y).
+    """
+    widget.move(pos)
diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py
index a568ec91771f01a8011ec2ffd48a47e7a9b7354a..433587a1248d644433df24ae7c7ddafda7448a80 100644
--- a/soitool/modules/module_table.py
+++ b/soitool/modules/module_table.py
@@ -1,13 +1,12 @@
-"""Module containing subclassed SOIModule (QTableWidget, ModuleBase)."""
+"""Module containing a general SOI-module table."""
 from PySide2.QtWidgets import QTableWidget, QTableWidgetItem
 from PySide2 import QtGui, QtCore
-from PySide2.QtGui import QIcon
-from soitool.modules.module_base import ModuleBase
-
-HEADER_FONT = QtGui.QFont()
-HEADER_FONT.setFamily("Arial")
-HEADER_FONT.setPointSize(12)
-HEADER_FONT.setWeight(100)
+from soitool.modules.module_base import (
+    ModuleBase,
+    resize_table,
+    get_table_size,
+    HEADLINE_FONT,
+)
 
 START_ROWS = 2
 START_COLUMNS = 2
@@ -20,17 +19,17 @@ class Meta(type(ModuleBase), type(QTableWidget)):
 class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
     """Modified QTableWidget.
 
-    Inherits from ModuleBase and QTableWidget.
-    ModuleBase is used as an interface, it's methods are overridden.
+    By default, the widget initializes as an empty START_ROWS * START_COLUMNS
+    table. If parameters are given, the table initializes accordingly:
+    'size' is a dict: {"width": int, "height": int},
+    'data' is a 2D list where data[x][y] represents row x, column y.
 
     The widget does not use more room than needed, and resizes dynamically.
     Columnheaders are styled with light grey background and bold text.
-    Has shortcuts for adding and removing rows and columns.
+    It has shortcuts for adding and removing rows and columns.
 
-    By default, the widget initializes as an empty START_ROWS * START_COLUMNS
-    table. If parameters are given, the table initializes accordingly:
-    'size' is a dict: {"width": int, "height": int},
-    'data' is a 2D list where content[x][y] represents row x, column y.
+    Inherits from ModuleBase and QTableWidget.
+    ModuleBase is used as an interface, it's methods are overridden.
     """
 
     def __init__(self, size=None, data=None):
@@ -50,10 +49,8 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
             self.setColumnCount(START_COLUMNS)
             self.setRowCount(START_ROWS)
 
-            # Resize width and height of columns and rows, & set size of window
-            self.resize()
-            self.setFixedWidth(START_COLUMNS * self.columnWidth(0) + 2)
-            self.setFixedHeight(START_ROWS * self.rowHeight(0) + 5)
+            # Resize width and height of rows, columns and window
+            resize_table(self)
 
             # Set header-items
             for i in range(self.columnCount()):
@@ -77,7 +74,7 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
             self.setFixedWidth(size["width"])
             self.setFixedHeight(size["height"])
 
-        self.cellChanged.connect(self.resize)
+        self.cellChanged.connect(resize_table(self))
 
     def keyPressEvent(self, event):
         """Launch actions when specific combinations of keys are pressed.
@@ -125,87 +122,35 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
         """
         item = QTableWidgetItem(text)
         item.setBackground(QtGui.QBrush(QtGui.QColor(220, 220, 220)))
-        item.setFont(HEADER_FONT)
+        item.setFont(HEADLINE_FONT)
         self.setItem(0, column, item)
 
     def add_column(self):
         """Add column to the right of selected column."""
         self.insertColumn(self.currentColumn() + 1)
         self.set_header_item(self.currentColumn() + 1, "")
-        self.resize()
+        resize_table(self)
 
     def remove_column(self):
         """Remove selected column if two or more columns exist."""
         if self.columnCount() > 1:
             self.removeColumn(self.currentColumn())
-            self.resize()
+            resize_table(self)
 
     def add_row(self):
         """Add row below selected row."""
         self.insertRow(self.currentRow() + 1)
-        self.resize()
+        resize_table(self)
 
     def remove_row(self):
         """Remove selected row if two or more rows exist (including header)."""
         if self.rowCount() > 2 and self.currentRow() != 0:
             self.removeRow(self.currentRow())
-            self.resize()
-
-    def resize(self):
-        """Resize widget, rows and columns.
-
-        Resize widget size to total width and height of rows and columns.
-        Resize rows and columns to contents.
-        """
-        self.resizeColumnsToContents()
-        self.resizeRowsToContents()
-
-        # Calculate total width and height of columns and rows
-        width = 0
-        height = 0
-
-        for x in range(self.columnCount()):
-            width += self.columnWidth(x) + 0.5
-
-        for y in range(self.rowCount()):
-            height += self.rowHeight(y) + 0.5
-
-        # Set total width and height
-        self.setFixedWidth(width)
-        self.setFixedHeight(height)
+            resize_table(self)
 
     def get_size(self):
-        """Get size of widget.
-
-        Returns
-        -------
-        Tuple
-            (width, height) (total)
-        """
-        # Calculate total width and height of columns and rows
-        width = 0
-        height = 0
-
-        for i in range(self.columnCount()):
-            width += self.columnWidth(i) + 0.5
-
-        for i in range(self.rowCount()):
-            height += self.rowHeight(i) + 0.5
-
-        return width, height
-
-    def set_pos(self, pos):
-        """Set position of widget.
-
-        Parameters
-        ----------
-        pos : QPoint
-            Position (x, y).
-        """
-        self.move(pos)
-
-    def render_onto_pdf(self):
-        """Render onto pdf."""
+        """Return size of widget."""
+        return get_table_size(self)
 
     def get_data(self):
         """Return list containing module data.
@@ -236,4 +181,4 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
     @staticmethod
     def get_icon():
         """Get icon of module."""
-        return QIcon("soitool/media/tablemodule.png")
+        return QtGui.QIcon("soitool/media/tablemodule.png")
diff --git a/soitool/new_module_dialog.py b/soitool/new_module_dialog.py
index 2138c97961ac469ca49f4e20ff2aa1473815edb3..2fd3501b3b27c606dac34482280c70d95d975138 100644
--- a/soitool/new_module_dialog.py
+++ b/soitool/new_module_dialog.py
@@ -12,35 +12,10 @@ from PySide2.QtWidgets import (
     QCheckBox,
 )
 from PySide2.QtCore import QSize, Qt
-from PySide2.QtGui import QIcon
 from soitool.modules.module_table import TableModule
-from soitool.modules.module_base import ModuleBase
-
-
-class ModulePlaceholder(ModuleBase):
-    """Dummy module used only to fill dialog with content while developing."""
-
-    def set_pos(self, pos):
-        """Not used."""
-        raise NotImplementedError
-
-    def get_size(self):
-        """Not used."""
-        raise NotImplementedError
-
-    def render_onto_pdf(self):
-        """Not used."""
-        raise NotImplementedError
-
-    @staticmethod
-    def get_user_friendly_name():
-        """Get placeholder name."""
-        return "Modul"
-
-    @staticmethod
-    def get_icon():
-        """Get standard placeholder icon."""
-        return QIcon("soitool/media/placeholder.png")
+from soitool.modules.module_authentication_board import (
+    AuthenticationBoardModule,
+)
 
 
 # Constant holding all modules the user can choose from. This is intended as a
@@ -48,9 +23,7 @@ class ModulePlaceholder(ModuleBase):
 # placed here, and the rest of the program will respect them.
 MODULE_CHOICES = [
     TableModule,
-    ModulePlaceholder,
-    ModulePlaceholder,
-    ModulePlaceholder,
+    AuthenticationBoardModule,
 ]
 
 
diff --git a/soitool/serialize_export_import_soi.py b/soitool/serialize_export_import_soi.py
index eb5dd8d35bf0df367255ce91cc392b2d5aae1c4f..a5c1dc80224023b4c042fa1343507e7bc955cf0a 100644
--- a/soitool/serialize_export_import_soi.py
+++ b/soitool/serialize_export_import_soi.py
@@ -5,6 +5,9 @@ from schema import Schema, And, Or
 from soitool.soi import SOI
 from soitool.compressor import compress, decompress
 from soitool.modules.module_table import TableModule
+from soitool.modules.module_authentication_board import (
+    AuthenticationBoardModule,
+)
 
 # Valid schema for serialized SOI
 SERIALIZED_SOI_SCHEMA = Schema(
@@ -218,6 +221,15 @@ def import_soi(file_path):
             modules.append(
                 {"widget": TableModule(size, data), "meta": module["meta"]}
             )
+        elif module_type == "AuthentificationBoardModule":
+            size = module["size"]
+            data = module["data"]
+            modules.append(
+                {
+                    "widget": AuthenticationBoardModule(size, data),
+                    "meta": module["meta"],
+                }
+            )
         else:
             raise TypeError(
                 "Module-type '{}' is not recognized.".format(module_type)
diff --git a/soitool/soi.py b/soitool/soi.py
index b9b7a724ea755598b95cccf10dabf4fc64475ab3..590b61e7e2f00ed4e9caf2a4b3ff04088f05e600 100644
--- a/soitool/soi.py
+++ b/soitool/soi.py
@@ -11,8 +11,9 @@ from rectpack import (
     skyline,
     guillotine,
 )
+from soitool.modules.module_base import set_module_pos
 
-# functions to sort modules by different criteria
+# Functions to sort modules by different criteria
 
 
 def modules_sort_by_none(modules):
@@ -381,7 +382,7 @@ class SOI:
             + scene_skip_distance_page_height
         )
 
-        module["widget"].set_pos(QPoint(new_x, new_y))
+        set_module_pos(module["widget"], QPoint(new_x, new_y))
 
     def get_module_with_name(self, name):
         """Return module with given name.
diff --git a/soitool/soi_workspace_widget.py b/soitool/soi_workspace_widget.py
index 520a3bbaed8d9261d9889fe5bc93f226a53bf5fb..5355999e61f2dad4d18d28c4c3bf9004990adf07 100644
--- a/soitool/soi_workspace_widget.py
+++ b/soitool/soi_workspace_widget.py
@@ -16,7 +16,7 @@ from soitool.soi import SOI, ModuleType, ModuleNameTaken
 from soitool.module_list import ModuleList
 from soitool.inline_editable_soi_view import InlineEditableSOIView
 from soitool.setup_settings import Setup
-from soitool.new_module_dialog import NewModuleDialog, ModulePlaceholder
+from soitool.new_module_dialog import NewModuleDialog
 from soitool.dialog_wrappers import exec_warning_dialog
 
 
@@ -102,42 +102,27 @@ class SOIWorkspaceWidget(QWidget):
             module_widget_implementation = chosen_module.widget_implementation
             is_attachment = new_module_dialog.checkbox_attachment.isChecked()
 
-            if module_widget_implementation is ModulePlaceholder:
+            # No module name means the user expects one to be generated
+            # Autogenerated name is not meant to be pretty, it's just meant
+            # to be unique
+            if not module_name:
+                module_name = "{} {}".format(
+                    module_choice,
+                    str(len(self.soi.modules) + len(self.soi.attachments) + 1),
+                )
+
+            try:
+                self.soi.add_module(
+                    module_name, module_widget_implementation(), is_attachment,
+                )
+            except ModuleNameTaken:
                 exec_warning_dialog(
                     text="Modulen ble ikke lagt til.",
-                    informative_text="Den valgte modulen er ikke "
-                    "implementert. Modulen er trolig bare valgbar for å fylle "
-                    "ut valgene til flere moduler er implementert.",
+                    informative_text="Navnet du valgte er allerede i "
+                    "bruk. Modulnavn må være unike. Velg et unikt "
+                    "modulnavn, eller la programmet lage et navn "
+                    "automatisk.",
                 )
-            else:
-                # no module name means the user expects one to be generated
-                # autogenerated name is not meant to be pretty, it's just meant
-                # to be unique
-                if not module_name:
-                    module_name = "{} {}".format(
-                        module_choice,
-                        str(
-                            len(self.soi.modules)
-                            + len(self.soi.attachments)
-                            + 1
-                        ),
-                    )
-
-                try:
-                    self.soi.add_module(
-                        module_name,
-                        module_widget_implementation(),
-                        is_attachment,
-                    )
-                except ModuleNameTaken:
-                    exec_warning_dialog(
-                        text="Modulen ble ikke lagt til.",
-                        informative_text="Navnet du valgte er allerede i "
-                        "bruk. Modulnavn må være unike. Velg et unikt "
-                        "modulnavn, eller la programmet lage et navn "
-                        "automatisk.",
-                    )
-
         elif dialogcode == QDialog.DialogCode.Rejected:
             pass
         else:
diff --git a/test/test_module_authentication_board.py b/test/test_module_authentication_board.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5f5869433a0dfe8c7ca36f467cee19e4a22c32a
--- /dev/null
+++ b/test/test_module_authentication_board.py
@@ -0,0 +1,172 @@
+"""Test AuthenticationBoardModule."""
+import unittest
+from PySide2 import QtGui
+from PySide2.QtWidgets import QApplication
+from PySide2.QtCore import Qt
+from PySide2.QtTest import QTest
+from soitool.modules.module_authentication_board import (
+    AuthenticationBoardModule,
+    START_NO_OF_AUTHENTICATION_CODES,
+    HEADLINE_TEXT,
+    ROW_IDENTIFIERS,
+    CODE_LENGTH,
+)
+from soitool.soi import SOI
+
+if isinstance(QtGui.qApp, type(None)):
+    app = QApplication([])
+else:
+    app = QtGui.qApp
+
+
+class TestDefaultAuthenticationBoardModule(unittest.TestCase):
+    """TestCase for AuthenticationBoardModule."""
+
+    def setUp(self):
+        """Create new AuthenticationBoardModule."""
+        self.module = AuthenticationBoardModule()
+
+    def test_default_module(self):
+        """Test that module is initialized properly."""
+        # Assert correct headline
+        self.assertEqual(self.module.item(0, 0).text(), HEADLINE_TEXT)
+
+        # Assert correct number of rows including header
+        self.assertEqual(
+            self.module.rowCount(), START_NO_OF_AUTHENTICATION_CODES + 1
+        )
+        # Assert correct number of columns
+        self.assertEqual(self.module.columnCount(), 3)
+
+        # Assert cell content in first column is correct
+        self.assertEqual(self.module.item(1, 0).text(), ROW_IDENTIFIERS[0])
+        self.assertEqual(self.module.item(2, 0).text(), ROW_IDENTIFIERS[1])
+
+        # Assert cell content in second column is correct
+        self.assertEqual(self.module.item(1, 1).text(), "0")
+        self.assertEqual(self.module.item(2, 1).text(), "1")
+
+        # Assert cell content in third column is correct
+        code = self.module.item(1, 2).text()
+        self.assertTrue(len(code) >= CODE_LENGTH)
+        # Assert all code characters are taken from list code_characters
+        for _, character in enumerate(code):
+            if character != " ":
+                self.assertTrue(character in self.module.code_characters)
+        # Assert all codes are unique
+        code_list = self.module.get_codes()
+        code_set = set(code_list)
+        self.assertEqual(len(code_list), len(code_set))
+
+    def test_generate_unique_authentification_code(self):
+        """Test function generate_unique_authentification_module."""
+        code_list = self.module.get_codes()
+        generated_code = self.module.generate_unique_authentification_code()
+
+        # Assert code length is equal to an existing code
+        self.assertEqual(len(generated_code), len(code_list[0]))
+        # Assert generated code is not equal to any existing codes
+        self.assertFalse(generated_code in code_list)
+
+    def test_get_codes(self):
+        """Test function get_codes."""
+        # Get codes
+        code_list = self.module.get_codes()
+
+        # Assert codes are correct
+        for i, code in enumerate(code_list):
+            self.assertEqual(code, self.module.item(i + 1, 2).text())
+
+    def test_gui_add_row(self):
+        """Test adding rows with shortcuts."""
+        # Widget must be shown for shortcuts to work
+        self.module.show()
+        QTest.qWaitForWindowExposed(self.module)
+
+        old_row_count = self.module.rowCount()
+
+        # Use shortcut 'Ctrl + +'
+        QTest.keyClicks(self.module, "+", Qt.ControlModifier)
+
+        # Assert a new row is added
+        new_row_count = self.module.rowCount()
+        self.assertEqual(old_row_count + 1, new_row_count)
+
+        # Assert new row has correct content
+        row_index = new_row_count - 1
+        self.assertEqual(
+            self.module.item(row_index, 0).text(),
+            ROW_IDENTIFIERS[row_index - 1],
+        )
+        self.assertEqual(
+            self.module.item(row_index, 1).text(), str(row_index - 1)
+        )
+        new_code = self.module.item(row_index, 2).text()
+        existing_code = self.module.item(1, 2).text()
+        self.assertEqual(len(new_code), len(existing_code))
+
+    def test_gui_remove_row(self):
+        """Test removing rows with shortcuts."""
+        # Widget must be shown for shortcuts to work
+        self.module.show()
+        QTest.qWaitForWindowExposed(self.module)
+
+        old_row_count = self.module.rowCount()
+
+        # First row is selected on startup, it contains headline
+        # and user should not be able to delete it with shortcut
+        QTest.keyClicks(self.module, "_", Qt.ControlModifier)
+        # Assert row was not removed
+        new_row_count = self.module.rowCount()
+        self.assertEqual(old_row_count, new_row_count)
+
+        # Move to first row, then use shortcut to delete it
+        QTest.keyClick(self.module, Qt.Key_Down)
+        QTest.keyClicks(self.module, "_", Qt.ControlModifier)
+        # Assert row was removed and replaced by the row below
+        value_to_delete = self.module.item(1, 1).text()
+        new_row_count = self.module.rowCount()
+        self.assertEqual(old_row_count - 1, new_row_count)
+        self.assertEqual(self.module.item(1, 1).text(), value_to_delete)
+
+        # Remove rows until only headline-row and a single code-row exist
+        for _ in range(1, self.module.rowCount() - 1):
+            QTest.keyClick(self.module, Qt.Key_Down)
+            QTest.keyClicks(self.module, "_", Qt.ControlModifier)
+        self.assertTrue(self.module.rowCount() == 2)
+        # Try to remove final code-row, should not work
+        QTest.keyClick(self.module, Qt.Key_Down)
+        QTest.keyClicks(self.module, "_", Qt.ControlModifier)
+        # Assert row was not removed
+        self.assertTrue(self.module.rowCount() == 2)
+
+    def test_add_to_soi_smoke_test(self):
+        """Test that module can be added to SOI."""
+        soi = SOI()
+        module_name = "Test name"
+        soi.add_module(module_name, self.module)
+        self.assertTrue(soi.module_name_taken(module_name))
+
+
+class TestAuthenticationBoardModuleFromData(unittest.TestCase):
+    """TestCase for initializing AuthenticationBoardModule from data."""
+
+    def test_create_from_data(self):
+        """Test creating AuthenticationBoardModule from data."""
+        test_data = [
+            "Headline text",
+            ["A", "0", "TEST CODE ONE"],
+            ["B", "1", "TEST CODE TWO"],
+        ]
+        test_size = {"width": 100, "height": 100}
+
+        module = AuthenticationBoardModule(size=test_size, data=test_data)
+
+        # Assert module contains expected data
+        self.assertEqual(module.item(0, 0).text(), "Headline text")
+        self.assertEqual(module.item(1, 0).text(), "A")
+        self.assertEqual(module.item(1, 1).text(), "0")
+        self.assertEqual(module.item(1, 2).text(), "TEST CODE ONE")
+        self.assertEqual(module.item(2, 0).text(), "B")
+        self.assertEqual(module.item(2, 1).text(), "1")
+        self.assertEqual(module.item(2, 2).text(), "TEST CODE TWO")