diff --git a/tests/test_invite.py b/tests/test_invite.py
index f3d652ac7f5dc7ab66ab126a9947e0eded1bad20..94f7967d7da3939faa1a6ad90b8f6fac977789ef 100644
--- a/tests/test_invite.py
+++ b/tests/test_invite.py
@@ -521,7 +521,7 @@ class TestInviteUseViews(UffdTestCase):
 		db.session.add(invite)
 		db.session.commit()
 		token = invite.token
-		r = self.client.get(path=url_for('invite.use', token=token), follow_redirects=True)
+		r = self.client.get(path=url_for('invite.use', invite_id=invite.id, token=token), follow_redirects=True)
 		dump('invite_use', r)
 		self.assertEqual(r.status_code, 200)
 
@@ -531,7 +531,7 @@ class TestInviteUseViews(UffdTestCase):
 		db.session.add(invite)
 		db.session.commit()
 		token = invite.token
-		r = self.client.get(path=url_for('invite.use', token=token), follow_redirects=True)
+		r = self.client.get(path=url_for('invite.use', invite_id=invite.id, token=token), follow_redirects=True)
 		dump('invite_use_loggedin', r)
 		self.assertEqual(r.status_code, 200)
 
@@ -540,7 +540,7 @@ class TestInviteUseViews(UffdTestCase):
 		db.session.add(invite)
 		db.session.commit()
 		token = invite.token
-		r = self.client.get(path=url_for('invite.use', token=token), follow_redirects=True)
+		r = self.client.get(path=url_for('invite.use', invite_id=invite.id, token=token), follow_redirects=True)
 		dump('invite_use_inactive', r)
 		self.assertEqual(r.status_code, 200)
 
@@ -562,6 +562,7 @@ class TestInviteUseViews(UffdTestCase):
 		db.session.add(invite)
 		db.session.commit()
 		ldap.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		self.assertIn(role0, user.roles)
 		self.assertNotIn(role1, user.roles)
@@ -570,7 +571,7 @@ class TestInviteUseViews(UffdTestCase):
 		self.assertNotIn(group1, user.groups)
 		self.assertFalse(invite.used)
 		self.login_as('user')
-		r = self.client.post(path=url_for('invite.grant', token=token), follow_redirects=True)
+		r = self.client.post(path=url_for('invite.grant', invite_id=invite_id, token=token), follow_redirects=True)
 		dump('invite_grant', r)
 		self.assertEqual(r.status_code, 200)
 		db_flush()
@@ -587,9 +588,10 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), disabled=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		self.login_as('user')
-		r = self.client.post(path=url_for('invite.grant', token=token), follow_redirects=True)
+		r = self.client.post(path=url_for('invite.grant', invite_id=invite_id, token=token), follow_redirects=True)
 		dump('invite_grant_invalid_invite', r)
 		self.assertEqual(r.status_code, 200)
 		self.assertFalse(Invite.query.filter_by(token=token).first().used)
@@ -598,9 +600,10 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60))
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		self.login_as('user')
-		r = self.client.post(path=url_for('invite.grant', token=token), follow_redirects=True)
+		r = self.client.post(path=url_for('invite.grant', invite_id=invite_id, token=token), follow_redirects=True)
 		dump('invite_grant_no_roles', r)
 		self.assertEqual(r.status_code, 200)
 		self.assertFalse(Invite.query.filter_by(token=token).first().used)
@@ -613,9 +616,10 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role])
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		self.login_as('user')
-		r = self.client.post(path=url_for('invite.grant', token=token), follow_redirects=True)
+		r = self.client.post(path=url_for('invite.grant', invite_id=invite_id, token=token), follow_redirects=True)
 		dump('invite_grant_no_new_roles', r)
 		self.assertEqual(r.status_code, 200)
 		self.assertFalse(Invite.query.filter_by(token=token).first().used)
@@ -633,8 +637,9 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
-		r = self.client.get(path=url_for('invite.signup_start', token=token), follow_redirects=True)
+		r = self.client.get(path=url_for('invite.signup_start', invite_id=invite_id, token=token), follow_redirects=True)
 		dump('invite_signup_start', r)
 		self.assertEqual(r.status_code, 200)
 		r = self.client.post(path=url_for('invite.signup_submit', token=token),
@@ -654,7 +659,7 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True)
 		db.session.add(invite)
 		db.session.commit()
-		r = self.client.get(path=url_for('invite.signup_start', token=invite.token), follow_redirects=True)
+		r = self.client.get(path=url_for('invite.signup_start', invite_id=invite.id, token=invite.token), follow_redirects=True)
 		dump('invite_signup_invalid_invite', r)
 		self.assertEqual(r.status_code, 200)
 
@@ -662,7 +667,7 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False)
 		db.session.add(invite)
 		db.session.commit()
-		r = self.client.get(path=url_for('invite.signup_start', token=invite.token), follow_redirects=True)
+		r = self.client.get(path=url_for('invite.signup_start', invite_id=invite.id, token=invite.token), follow_redirects=True)
 		dump('invite_signup_nosignup', r)
 		self.assertEqual(r.status_code, 200)
 
@@ -670,7 +675,7 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
-		r = self.client.post(path=url_for('invite.signup_submit', token=invite.token),
+		r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token),
 			data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com',
 			      'password1': 'notsecret', 'password2': 'notthesame'}, follow_redirects=True)
 		dump('invite_signup_wrongpassword', r)
@@ -680,7 +685,7 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
-		r = self.client.post(path=url_for('invite.signup_submit', token=invite.token),
+		r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token),
 			data={'loginname': '', 'displayname': 'New User', 'mail': 'test@example.com',
 			      'password1': 'notsecret', 'password2': 'notsecret'}, follow_redirects=True)
 		dump('invite_signup_invalid', r)
@@ -691,7 +696,7 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
-		r = self.client.post(path=url_for('invite.signup_submit', token=invite.token),
+		r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token),
 			data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com',
 			      'password1': 'notsecret', 'password2': 'notsecret'}, follow_redirects=True)
 		dump('invite_signup_mailerror', r)
@@ -701,14 +706,15 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		for i in range(20):
-			r = self.client.post(path=url_for('invite.signup_submit', token=token),
+			r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite_id, token=token),
 				data={'loginname': 'newuser%d'%i, 'displayname': 'New User', 'mail': 'test%d@example.com'%i,
 							'password1': 'notsecret', 'password2': 'notsecret'}, follow_redirects=True)
 			self.assertEqual(r.status_code, 200)
 		self.app.last_mail = None
-		r = self.client.post(path=url_for('invite.signup_submit', token=token),
+		r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite_id, token=token),
 			data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com',
 			      'password1': 'notsecret', 'password2': 'notsecret'}, follow_redirects=True)
 		dump('invite_signup_hostlimit', r)
@@ -720,14 +726,15 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		for i in range(3):
-			r = self.client.post(path=url_for('invite.signup_submit', token=token),
+			r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite_id, token=token),
 				data={'loginname': 'newuser%d'%i, 'displayname': 'New User', 'mail': 'test@example.com',
 							'password1': 'notsecret', 'password2': 'notsecret'}, follow_redirects=True)
 			self.assertEqual(r.status_code, 200)
 		self.app.last_mail = None
-		r = self.client.post(path=url_for('invite.signup_submit', token=token),
+		r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite_id, token=token),
 			data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com',
 			      'password1': 'notsecret', 'password2': 'notsecret'}, follow_redirects=True)
 		dump('invite_signup_maillimit', r)
@@ -739,8 +746,9 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
-		r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+		r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 		                     data={'loginname': 'newuser'})
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.content_type, 'application/json')
@@ -750,8 +758,9 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
-		r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+		r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 		                     data={'loginname': ''})
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.content_type, 'application/json')
@@ -761,8 +770,9 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
-		r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+		r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 		                     data={'loginname': 'testuser'})
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.content_type, 'application/json')
@@ -772,8 +782,9 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
-		r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+		r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 		                     data={'loginname': 'testuser'})
 		self.assertEqual(r.status_code, 403)
 		self.assertEqual(r.content_type, 'application/json')
@@ -783,8 +794,9 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
-		r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+		r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 		                     data={'loginname': 'testuser'})
 		self.assertEqual(r.status_code, 403)
 		self.assertEqual(r.content_type, 'application/json')
@@ -794,13 +806,14 @@ class TestInviteUseViews(UffdTestCase):
 		invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True)
 		db.session.add(invite)
 		db.session.commit()
+		invite_id = invite.id
 		token = invite.token
 		for i in range(20):
-			r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+			r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 													 data={'loginname': 'testuser'})
 			self.assertEqual(r.status_code, 200)
 			self.assertEqual(r.content_type, 'application/json')
-		r = self.client.post(path=url_for('invite.signup_check', token=token), follow_redirects=True,
+		r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True,
 		                     data={'loginname': 'testuser'})
 		self.assertEqual(r.status_code, 200)
 		self.assertEqual(r.content_type, 'application/json')
diff --git a/uffd/invite/templates/invite/list.html b/uffd/invite/templates/invite/list.html
index 2eb543541918806337096048903d4de87509c98b..a527fd45e4477ed4fa5fb85313f7126f356cff94 100644
--- a/uffd/invite/templates/invite/list.html
+++ b/uffd/invite/templates/invite/list.html
@@ -22,8 +22,8 @@
 			<tr>
 				<td>
 					{% if invite.creator == request.user and invite.active %}
-					<a href="{{ url_for('invite.use', token=invite.token) }}"><code>{{ invite.short_token }}</code></a>
-					<button type="button" class="btn btn-link btn-sm p-0 copy-clipboard" data-copy="{{ url_for('invite.use', token=invite.token, _external=True) }}" title="{{_('Copy link to clipboard')}}"><i class="fas fa-clipboard"></i></button>
+					<a href="{{ url_for('invite.use', invite_id=invite.id, token=invite.token) }}"><code>{{ invite.short_token }}</code></a>
+					<button type="button" class="btn btn-link btn-sm p-0 copy-clipboard" data-copy="{{ url_for('invite.use', invite_id=invite.id, token=invite.token, _external=True) }}" title="{{_('Copy link to clipboard')}}"><i class="fas fa-clipboard"></i></button>
 					<button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}-qrcode" title="{{_('Show link as QR code')}}"><i class="fas fa-qrcode"></i></button>
 					{% else %}
 					<code>{{ invite.short_token }}</code>
@@ -143,7 +143,7 @@
 				</button>
 			</div>
 			<div class="modal-body">
-				{{ url_for('invite.use', token=invite.token, _external=True)|qrcode_svg(width='100%', height='100%') }}
+				{{ url_for('invite.use', invite_id=invite.id, token=invite.token, _external=True)|qrcode_svg(width='100%', height='100%') }}
 			</div>
 		</div>
 	</div>
diff --git a/uffd/invite/templates/invite/use.html b/uffd/invite/templates/invite/use.html
index 687153f9ab969b34507f8c4f55c91d24fbbe2388..f8bb7278e6bb95fd11a31665153e3ca6e10065c1 100644
--- a/uffd/invite/templates/invite/use.html
+++ b/uffd/invite/templates/invite/use.html
@@ -24,17 +24,17 @@
 	{% endif %}
 	{% if request.user %}
 		{% if invite.roles %}
-			<form method="POST" action="{{ url_for("invite.grant", token=invite.token) }}" class="mb-2">
+			<form method="POST" action="{{ url_for("invite.grant", invite_id=invite.id, token=invite.token) }}" class="mb-2">
 				<button type="submit" class="btn btn-primary btn-block">{{_('Add the roles to your account now')}}</button>
 			</form>
 			<a href="{{ url_for("session.logout", ref=url_for("session.login", ref=request.full_path)) }}" class="btn btn-secondary btn-block">{{_('Logout and switch to a different account')}}</a>
 		{% endif %}
 		{% if invite.allow_signup %}
-			<a href="{{ url_for("session.logout", ref=url_for("invite.signup_start", token=invite.token)) }}" class="btn btn-secondary btn-block">{{_('Logout to register a new account')}}</a>
+			<a href="{{ url_for("session.logout", ref=url_for("invite.signup_start", invite_id=invite.id, token=invite.token)) }}" class="btn btn-secondary btn-block">{{_('Logout to register a new account')}}</a>
 		{% endif %}
 	{% else %}
 		{% if invite.allow_signup %}
-			<a href="{{ url_for("invite.signup_start", token=invite.token) }}" class="btn btn-primary btn-block">{{_('Register a new account')}}</a>
+			<a href="{{ url_for("invite.signup_start", invite_id=invite.id, token=invite.token) }}" class="btn btn-primary btn-block">{{_('Register a new account')}}</a>
 		{% endif %}
 		{% if invite.roles %}
 			<a href="{{ url_for("session.login", ref=request.full_path) }}" class="btn btn-primary btn-block">{{_('Login and add the roles to your account')}}</a>
diff --git a/uffd/invite/views.py b/uffd/invite/views.py
index 2c0b7f2754f7af93849fdf86542f82eaaaa348f3..185d1a224044a239596c747d6da2a2ec7c911cc6 100644
--- a/uffd/invite/views.py
+++ b/uffd/invite/views.py
@@ -1,6 +1,7 @@
 import datetime
+import secrets
 
-from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
+from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, abort
 from flask_babel import gettext as _, lazy_gettext
 import sqlalchemy
 
@@ -102,19 +103,34 @@ def reset(invite_id):
 	db.session.commit()
 	return redirect(url_for('.index'))
 
+# Deprecated
 @bp.route('/<token>')
-def use(token):
-	invite = Invite.query.filter_by(token=token).first_or_404()
+def use_legacy(token):
+	matching_invite = None
+	for invite in Invite.query.filter(Invite.valid_until > datetime.datetime.now().replace(second=0, microsecond=0)):
+		if secrets.compare_digest(invite.token, token):
+			matching_invite = invite
+	if not matching_invite:
+		abort(404)
+	return redirect(url_for('invite.use', invite_id=matching_invite.id, token=token))
+
+@bp.route('/<int:invite_id>/<token>')
+def use(invite_id, token):
+	invite = Invite.query.get(invite_id)
+	if not invite or not secrets.compare_digest(invite.token, token):
+		abort(404)
 	if not invite.active:
 		flash(_('Invalid invite link'))
 		return redirect('/')
 	return render_template('invite/use.html', invite=invite)
 
-@bp.route('/<token>/grant', methods=['POST'])
+@bp.route('/<int:invite_id>/<token>/grant', methods=['POST'])
 @login_required(selfservice_acl_check)
 @csrf_protect(blueprint=bp)
-def grant(token):
-	invite = Invite.query.filter_by(token=token).first_or_404()
+def grant(invite_id, token):
+	invite = Invite.query.get(invite_id)
+	if not invite or not secrets.compare_digest(invite.token, token):
+		abort(404)
 	invite_grant = InviteGrant(invite=invite, user=request.user)
 	db.session.add(invite_grant)
 	success, msg = invite_grant.apply()
@@ -128,12 +144,17 @@ def grant(token):
 
 @bp.url_defaults
 def inject_invite_token(endpoint, values):
-	if endpoint in ['invite.signup_submit', 'invite.signup_check'] and 'token' in request.view_args:
-		values['token'] = request.view_args['token']
-
-@bp.route('/<token>/signup')
-def signup_start(token):
-	invite = Invite.query.filter_by(token=token).first_or_404()
+	if endpoint in ['invite.signup_submit', 'invite.signup_check']:
+		if 'invite_id' in request.view_args:
+			values['invite_id'] = request.view_args['invite_id']
+		if 'token' in request.view_args:
+			values['token'] = request.view_args['token']
+
+@bp.route('/<int:invite_id>/<token>/signup')
+def signup_start(invite_id, token):
+	invite = Invite.query.get(invite_id)
+	if not invite or not secrets.compare_digest(invite.token, token):
+		abort(404)
 	if not invite.active:
 		flash(_('Invalid invite link'))
 		return redirect('/')
@@ -142,12 +163,14 @@ def signup_start(token):
 		return redirect('/')
 	return render_template('signup/start.html')
 
-@bp.route('/<token>/signupcheck', methods=['POST'])
-def signup_check(token):
+@bp.route('/<int:invite_id>/<token>/signupcheck', methods=['POST'])
+def signup_check(invite_id, token):
 	if host_ratelimit.get_delay():
 		return jsonify({'status': 'ratelimited'})
 	host_ratelimit.log()
-	invite = Invite.query.filter_by(token=token).first_or_404()
+	invite = Invite.query.get(invite_id)
+	if not invite or not secrets.compare_digest(invite.token, token):
+		abort(404)
 	if not invite.active or not invite.allow_signup:
 		return jsonify({'status': 'error'}), 403
 	if not User().set_loginname(request.form['loginname']):
@@ -156,9 +179,11 @@ def signup_check(token):
 		return jsonify({'status': 'exists'})
 	return jsonify({'status': 'ok'})
 
-@bp.route('/<token>/signup', methods=['POST'])
-def signup_submit(token):
-	invite = Invite.query.filter_by(token=token).first_or_404()
+@bp.route('/<int:invite_id>/<token>/signup', methods=['POST'])
+def signup_submit(invite_id, token):
+	invite = Invite.query.get(invite_id)
+	if not invite or not secrets.compare_digest(invite.token, token):
+		abort(404)
 	if request.form['password1'] != request.form['password2']:
 		return render_template('signup/start.html', error=_('Passwords do not match'))
 	signup_delay = signup_ratelimit.get_delay(request.form['mail'])