diff --git a/roac/__init__.py b/roac/__init__.py
index 525114ce179ad480ecc15a7cd7371258cdc2a872..d48f9c36c179a9a1b14ac9fb17980ae707daf057 100644
--- a/roac/__init__.py
+++ b/roac/__init__.py
@@ -1,10 +1,12 @@
 import secrets
 import os
+import datetime
+import functools
 
-from flask import Flask
+from flask import Flask, session, request, redirect, abort, Response, url_for, render_template
+from requests_oauthlib import OAuth2Session
 
 from .models import *
-from .base import bp as base
 
 def create_app(test_config=None):
 	app = Flask(__name__)
@@ -15,9 +17,66 @@ def create_app(test_config=None):
 		app.config.from_pyfile('config.py', silent=True)
 	else:
 		app.config.from_mapping(test_config)
-
+	# OAuth2Session.fetch_token verifies that the passed URIs scheme (the scheme
+	# of request.url) is HTTPS. The way we deploy this app, request.url does not
+	# reflect the actual request url, so we disable this check.
+	if app.debug:
+		os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
 	db.init_app(app)
 
+	def user_required(func):
+		@functools.wraps(func)
+		def decorator(*args, **kwargs):
+			timestamp = datetime.datetime.fromtimestamp(session.get('timestamp', 0))
+			if datetime.datetime.utcnow() - timestamp > datetime.timedelta(minutes=30):
+				return redirect(url_for('oauth_login'))
+			if 'userinfo' not in session:
+				return redirect(url_for('oauth_login'))
+			user = User.query.filter_by(loginname=session['userinfo']['nickname']).one_or_none()
+			if user is None:
+				return render_template('error_unknown_user.html')
+			return func(*args, **kwargs)
+		return decorator
+
+	@app.route('/')
+	@user_required
+	def index():
+		return render_template('index.html')
+
+	@app.route('/create')
+	@user_required
+	def create_account():
+		return render_template('index.html')
+
+	@app.route('/oauth/login')
+	def oauth_login():
+		client = OAuth2Session(app.config['OAUTH2_CLIENT_ID'],
+		                       redirect_uri=url_for('oauth_callback', _external=True))
+		url, state = client.authorization_url(app.config['OAUTH2_AUTH_URL'])
+		session.clear()
+		session['timestamp'] = datetime.datetime.utcnow().timestamp()
+		session['oauth-state'] = state
+		return redirect(url)
+
+	@app.route('/oauth/callback')
+	def oauth_callback():
+		if 'oauth-state' not in session:
+			return redirect(url_for('oauth_login'))
+		timestamp = datetime.datetime.fromtimestamp(session.get('timestamp', 0))
+		if datetime.datetime.utcnow() - timestamp > datetime.timedelta(minutes=30):
+			return redirect(url_for('oauth_login'))
+		client = OAuth2Session(app.config['OAUTH2_CLIENT_ID'],
+		                       redirect_uri=url_for('oauth_callback', _external=True),
+		                       state=session['oauth-state'])
+		client.fetch_token(app.config['OAUTH2_TOKEN_URL'],
+		                   client_secret=app.config['OAUTH2_CLIENT_SECRET'],
+		                   authorization_response=request.url, verify=(not app.debug))
+		userinfo = client.get(app.config['OAUTH2_USERINFO_URL']).json()
+		session.clear()
+		session['timestamp'] = datetime.datetime.utcnow().timestamp()
+		session['userinfo'] = userinfo
+		return redirect(url_for('index'))
+
 	os.makedirs(app.instance_path, exist_ok=True)
 	with app.app_context():
 		db.create_all()
diff --git a/roac/default_config.py b/roac/default_config.py
index 903cfeb172394720b221b1f866fb78b50b65110e..de889fee1cf8e158a80a38b7bb92eb322e014edb 100644
--- a/roac/default_config.py
+++ b/roac/default_config.py
@@ -1 +1,11 @@
-SQLALCHEMY_TRACK_MODIFICATIONS=False
+# URLs of the OAuth2-based identity provider
+OAUTH2_AUTH_URL = 'http://localhost:5000/oauth2/authorize'
+OAUTH2_TOKEN_URL = 'http://localhost:5000/oauth2/token'
+OAUTH2_USERINFO_URL = 'http://localhost:5000/oauth2/userinfo'
+OAUTH2_CLIENT_ID = 'roac'
+OAUTH2_CLIENT_SECRET = 'testsecret'
+
+# CSRF protection
+SESSION_COOKIE_SECURE=True
+SESSION_COOKIE_HTTPONLY=True
+SESSION_COOKIE_SAMESITE='Strict'
diff --git a/roac/models.py b/roac/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c73f1a31e8f83370cb999de4329bd30de841b3b
--- /dev/null
+++ b/roac/models.py
@@ -0,0 +1,9 @@
+from flask_sqlalchemy import SQLAlchemy
+
+db = SQLAlchemy()
+
+class User(db.Model):
+	__tablename__ = 'user'
+	id = db.Column(db.Integer(), primary_key=True, autoincrement=True)
+	loginname = db.Column(db.String, nullable=False)
+	rocketchat_id = db.Column(db.String, nullable=True)