diff --git a/auth.py b/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..1faf872c8e8e01ef1ad6623a19a85d8a457c6545
--- /dev/null
+++ b/auth.py
@@ -0,0 +1,67 @@
+import ldap
+import hmac, hashlib
+
+class User:
+    def __init__(self, username, groups):
+        self.username = username
+        self.groups = groups
+
+    def summarize(self):
+        return "{}:{}".format(self.username, ",".join(self.groups))
+
+    @staticmethod
+    def from_summary(summary):
+        name, groupstring = summary.split(":", 1)
+        groups = groupstring.split(",")
+        return User(name, groups)
+
+    @staticmethod
+    def from_hashstring(secure_string):
+        summary, hash = secure_string.split("=", 1)
+        return User.from_summary(summary)
+
+class LdapManager:
+    def __init__(self, url, base):
+        self.connection = ldap.initialize(url)
+        self.base = base
+
+    def login(self, username, password):
+        if not self.authenticate(username, password):
+            return None
+        groups = list(map(lambda g: g.decode("utf-8"), self.groups(username)))
+        print(groups)
+        return User(username, groups)
+
+    def authenticate(self, username, password):
+        try:
+            self.connection.simple_bind_s("uid={},ou=users,{}".format(username, self.base), password)
+            return True
+        except ldap.INVALID_CREDENTIALS:
+            return False
+        return False
+
+    def groups(self, username):
+        result = []
+        for _, result_dict in self.connection.search_s(self.base, ldap.SCOPE_SUBTREE, "(memberUid={})".format(username), ["cn"]):
+            result.append(result_dict["cn"][0])
+        return result
+
+class SecurityManager:
+    def __init__(self, key):
+        self.maccer = hmac.new(key.encode("utf-8"), digestmod=hashlib.sha512)
+
+    def hash_user(self, user):
+        maccer = self.maccer.copy()
+        summary = user.summarize()
+        maccer.update(summary.encode("utf-8"))
+        return "{}={}".format(summary, maccer.hexdigest())
+
+    def check_user(self, string):
+        parts = string.split("=", 1)
+        if len(parts) != 2:
+            # wrong format, expecting summary:hash
+            return False
+        summary, hash = map(lambda s: s.encode("utf-8"), parts)
+        maccer = self.maccer.copy()
+        maccer.update(summary)
+        return hmac.compare_digest(maccer.hexdigest().encode("utf-8"), hash)
diff --git a/requirements.txt b/requirements.txt
index ea72be555d893bfa30ec83484869df860093bf73..928fb5c88ed319661823139d7094bf5c06c041dd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,37 @@
-alembic
-blinker
-Flask
-Flask-Migrate
-Flask-Script
-Flask-SQLAlchemy
-Flask-WTF
-Flask-SocketIO
-Jinja2
-Mako
-MarkupSafe
-psycopg2
-python-editor
-SQLAlchemy
-Werkzeug
-wheel
-WTForms
-celery[redis]
-regex
-watchdog
+alembic==0.8.10
+amqp==2.1.4
+appdirs==1.4.0
+argh==0.26.2
+billiard==3.5.0.2
+blinker==1.4
+celery==4.0.2
+click==6.7
+Flask==0.12
+Flask-Migrate==2.0.3
+Flask-Script==2.0.5
+Flask-SocketIO==2.8.4
+Flask-SQLAlchemy==2.1
+Flask-WTF==0.14.2
+itsdangerous==0.24
+Jinja2==2.9.5
+kombu==4.0.2
+Mako==1.0.6
+MarkupSafe==0.23
+packaging==16.8
+pathtools==0.1.2
+psycopg2==2.6.2
+pyldap==2.4.28
+pyparsing==2.1.10
+python-editor==1.0.3
+python-engineio==1.2.2
+python-socketio==1.7.1
+pytz==2016.10
+PyYAML==3.12
+redis==2.10.5
+regex==2017.2.8
+six==1.10.0
+SQLAlchemy==1.1.5
+vine==1.1.3
+watchdog==0.8.3
+Werkzeug==0.11.15
+WTForms==2.1
diff --git a/server.py b/server.py
index 54e202d183db1b8ef4b2d6b57ac4bdc0f6544c24..c5385205793ff48453150569cb8b443d66038265 100755
--- a/server.py
+++ b/server.py
@@ -7,10 +7,12 @@ from flask_script import Manager, prompt
 from flask_migrate import Migrate, MigrateCommand
 #from flask_socketio import SocketIO
 from celery import Celery
+from functools import wraps
 
 import config
-from shared import db, date_filter, datetime_filter
+from shared import db, date_filter, datetime_filter, ldap_manager, security_manager
 from utils import is_past, mail_manager, url_manager
+from views.forms import LoginForm
 
 app = Flask(__name__)
 app.config.from_object(config)
@@ -35,26 +37,63 @@ app.jinja_env.lstrip_blocks = True
 app.jinja_env.filters["datify"] = date_filter
 app.jinja_env.filters["datetimify"] = datetime_filter
 app.jinja_env.filters["url_complete"] = url_manager.complete
+app.jinja_env.tests["auth_valid"] = security_manager.check_user
 
 import tasks
 
+from auth import User
+
+def check_login():
+    return "auth" in session and security_manager.check_user(session["auth"])
+def current_user():
+    if not check_login():
+        return None
+    return User.from_hashstring(session["auth"])
+
+def login_required(function):
+    @wraps(function)
+    def decorated_function(*args, **kwargs):
+        if check_login():
+            return function(*args, **kwargs)
+        else:
+            return redirect(url_for("login", next=request.url))
+    return decorated_function
+
+app.jinja_env.globals.update(check_login=check_login)
+app.jinja_env.globals.update(current_user=current_user)
+
 # blueprints here
 
 @app.route("/")
+@login_required
 def index():
     return render_template("index.html")
 
-@app.route("/imprint/")
-def imprint():
-    return render_template("imprint.html")
+@app.route("/login", methods=["GET", "POST"])
+def login():
+    if "auth" in session:
+        flash("You are already logged in.", "alert-success")
+        return redirect(url_for(request.args.get("next") or "index"))
+    form = LoginForm()
+    if form.validate_on_submit():
+        user = ldap_manager.login(form.username.data, form.password.data)
+        if user is not None:
+            session["auth"] = security_manager.hash_user(user)
+            flash("Login successful, {}!".format(user.username), "alert-success")
+            return redirect(request.args.get("next") or url_for("index"))
+        else:
+            flash("Wrong login data. Try again.", "alert-error")
+    return render_template("login.html", form=form)
 
-@app.route("/contact/")
-def contact():
-    return render_template("contact.html")
+@app.route("/logout")
+@login_required
+def logout():
+    if "auth" in session:
+        session.pop("auth")
+    else:
+        flash("You are not logged in.", "alert-error")
+    return redirect(url_for(".index"))
 
-@app.route("/privacy/")
-def privacy():
-    return render_template("privacy.html")
 
 if __name__ == "__main__":
     manager.run()
diff --git a/shared.py b/shared.py
index d3d79364de14a3a8e18fa8d89aa672be008df95e..9db6902009e882d6a34adeb44e86cf39af617b3f 100644
--- a/shared.py
+++ b/shared.py
@@ -71,3 +71,7 @@ def date_filter_long(date):
     return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W")
 def time_filter(time):
     return time.strftime("%H:%m")
+
+from auth import LdapManager, SecurityManager
+ldap_manager = LdapManager(config.LDAP_PROVIDER_URL, config.LDAP_BASE)
+security_manager = SecurityManager(config.SECURITY_KEY)
diff --git a/templates/layout.html b/templates/layout.html
index 1137842421915f8bc8664c1812493341f88254bc..95c63bd1ef62de301888445121cc4f0483015fdb 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -29,6 +29,13 @@
                 <li><a href="{{url_for("index")}}">Zuhause</a></li>
                 {# todo: add more links #}
             </ul>
+            <ul class="nav navbar-nav navbar-right">
+                {% if check_login() %}
+                <li><a href="{{url_for("logout")}}">Logout</a></li>
+                {% else %}
+                <li><a href="{{url_for("login")}}">Login</a></li>
+                {% endif %}
+            </ul>
         </div>
     </div>
 </nav>
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..fa86d85d59518580a5205050809aa05d7cdd3cc1
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,7 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Login{% endblock %}
+
+{% block content %}
+    {{render_form(form)}}
+{% endblock %}
diff --git a/templates/macros.html b/templates/macros.html
new file mode 100644
index 0000000000000000000000000000000000000000..bc416ca5b18255e28a0a76d8e922b62f4e7062e6
--- /dev/null
+++ b/templates/macros.html
@@ -0,0 +1,109 @@
+<!-- Taken from https://gist.github.com/bearz/7394681 and modified
+to not render a label for the CRSFTokenField -->
+
+{# Renders field for bootstrap 3 standards.
+
+    Params:
+        field - WTForm field
+        kwargs - pass any arguments you want in order to put them into the html attributes.
+        There are few exceptions: for - for_, class - class_, class__ - class_
+
+    Example usage:
+        {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
+#}
+{% macro render_field(field, label_visible=true) -%}
+
+    <div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
+        {% if field.type != 'HiddenField' and field.type !='CSRFTokenField' and label_visible %}
+            <label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
+            <!--<span onclick="el=document.getElementById('{{field.id}}-description');el.style.display=(el.style.display=='none'?'flex':'none')" class="field-description-questionmark">?</span>-->
+        {% endif %}
+        {{ field(title=field.description, class_='form-control', **kwargs) }}
+        {% if field.errors %}
+            {% for e in field.errors %}
+                <p class="help-block">{{ e }}</p>
+            {% endfor %}
+        {% endif %}
+    </div>
+    <div id="{{field.id}}-description" style="display:none" class="field-description">{{field.description}}</div>
+{%- endmacro %}
+
+{# Renders checkbox fields since they are represented differently in bootstrap
+    Params:
+        field - WTForm field (there are no check, but you should put here only BooleanField.
+        kwargs - pass any arguments you want in order to put them into the html attributes.
+        There are few exceptions: for - for_, class - class_, class__ - class_
+
+    Example usage:
+        {{ macros.render_checkbox_field(form.remember_me) }}
+ #}
+{% macro render_checkbox_field(field) -%}
+    <div class="checkbox {% if field.errors %}has-error{% endif %}">
+        <label>
+            {{ field(type='checkbox', **kwargs) }} {{ field.label }} 
+        </label>
+        <span onclick="el=document.getElementById('{{field.id}}-description');el.style.display=(el.style.display=='none'?'flex':'none')" class="field-description-questionmark">?</span>
+        {% if field.errors %}
+            {% for e in field.errors %}
+                <p class="help-block">{{ e }}</p>
+            {% endfor %}
+        {% endif %}
+    </div>
+    <div id="{{field.id}}-description" style="display:none" class="field-description">{{field.description}}</div>
+{%- endmacro %}
+
+{# Renders radio field
+    Params:
+        field - WTForm field (there are no check, but you should put here only BooleanField.
+        kwargs - pass any arguments you want in order to put them into the html attributes.
+        There are few exceptions: for - for_, class - class_, class__ - class_
+
+    Example usage:
+        {{ macros.render_radio_field(form.answers) }}
+ #}
+{% macro render_radio_field(field) -%}
+    {% for value, label, _ in field.iter_choices() %}
+        <div class="radio">
+            <label>
+                <input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}">{{ label }}
+            </label>
+        </div>
+    {% endfor %}
+{%- endmacro %}
+
+{# Renders WTForm in bootstrap way. There are two ways to call function:
+     - as macros: it will render all field forms using cycle to iterate over them
+     - as call: it will insert form fields as you specify:
+     e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login',
+                                        class_='login-form') %}
+                {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
+                {{ macros.render_field(form.password, placeholder='Input password', type='password') }}
+                {{ macros.render_checkbox_field(form.remember_me, type='checkbox') }}
+            {% endcall %}
+
+     Params:
+        form - WTForm class
+        action_url - url where to submit this form
+        action_text - text of submit button
+        class_ - sets a class for form
+    #}
+{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default') -%}
+
+    <form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}">
+        {{ form.hidden_tag() if form.hidden_tag }}
+        {% if caller %}
+            {{ caller() }}
+        {% else %}
+            {% for f in form %}
+                {% if f.type == 'BooleanField' %}
+                    {{ render_checkbox_field(f) }}
+                {% elif f.type == 'RadioField' %}
+                    {{ render_radio_field(f) }}
+                {% else %}
+                    {{ render_field(f) }}
+                {% endif %}
+            {% endfor %}
+        {% endif %}
+        <button type="submit" class="{{ btn_class }}">{{ action_text }} </button>
+    </form>
+{%- endmacro %}
diff --git a/views/forms.py b/views/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..175fcc24c2d558e2ebf621f31f4d9918406495a3
--- /dev/null
+++ b/views/forms.py
@@ -0,0 +1,7 @@
+from flask_wtf import FlaskForm
+from wtforms import StringField, PasswordField
+from wtforms.validators import InputRequired
+
+class LoginForm(FlaskForm):
+    username = StringField("User", validators=[InputRequired("Please input the username.")])
+    password = PasswordField("Password", validators=[InputRequired("Please input the password.")])