2. Administrative Interfaces


To every management controller you can add the parameter httperror followed by an HTTP error code. If LinOTP would return HTTP 200/OK with status: false in the JSON response indicating an internal error then LinOTP will instead return a e.g. HTTP 500 error code.

2.1. Admin Interface

Managing tokens can be performed via an HTTPs Interface just like the authentication via /validate/check and /validate/simplecheck. The main API is located at the controller /admin. There you can find functions like this:

  • /admin/init: Enroll token
  • /admin/enable: Enable token
  • /admin/disable: Disable token
  • /admin/show: List tokens
  • /admin/delete: Delete tokens

Other controllers are

  • /manage: provides the Web UI.
  • /system: provides the functions to configure the LinOTP server.
  • /license: provides the functions to set and get license information.
  • /audit: provides the functions for the audit trail.

2.1.1. Orphanced tokens

Orphaned tokens are tokens, that are still assigned to the user, but where the user object was deleted from the user database.

Using the method /admin/show you can also view such orphaned tokens by calling the method:


You can also find tokens that have no users or tokens that are assigned to no realm:



2.2. Admin Controller

See documentation for AdminController class: linotp.controllers.admin.AdminController

2.3. System Controller

See documentation for SystemController class: linotp.controllers.system.SystemController

2.4. Session parameter

The LinOTP administrative API (e.g. /manage, /admin, /system) requires an additional session parameter and admin_session cookie. This provides CSRF (Cross Site Request Forgery) protection. Then only requirement is that both values match but they should be sufficiently random so that an attacker can not simply guess it.

2.4.1. Accessing the API with your Browser

To access the administrative API in your Browser first open the Manage UI (https://FQDN/manage) and enter your admin user credentials. Then check the value of the admin_session cookie.

In Firefox this can be done as follows:

  1. Open the menu Tools/Page Info
  2. Open the tab Security
  3. Click on the View Cookies button
  4. Select the admin_session cookie
  5. Copy the content (something like 90dd0f1ac...).


Advanced users can also open a JavaScript console (e.g. Firebug) and execute the getsession() function.

Then open another browser window, type in the API call you want to make and append session. For example:


2.4.2. Programatically calling the API

To call the administrative API from a script you need to use a programming language/library that allows you to make HTTPS requests with Digest Authentication and allows you to set cookies.

Below is a fully functional Python example using the requests library to get a list of all token:

# -*- coding: utf-8 -*-
Small example on how to use the LinOTP administrative API. Prints all found
token serial numbers.

Tested with Python 2.7.9 and requests 2.4.3
import os
import binascii
import requests

# Adapt this for your server
BASE_URL = 'https://FQDN'
ADMIN_USER = 'admin'
ADMIN_PWD = 'secret'

# 'admin/show' returns information for all token in the system
url = BASE_URL + '/admin/show'
# Use Digest Authentication (checked by Apache Webserver or similar)
auth = requests.auth.HTTPDigestAuth(ADMIN_USER, ADMIN_PWD)
# Generate a random value for 'session'
random_val = binascii.hexlify(os.urandom(64))
# Instead of generating 'random_val' you can also make a request to
# https://FQDN/admin/getsession which will set a 'admin_session' cookie on
# the response. You can then extract this value and use it in further
# requests.
# Set that value both as parameter and as cookie
params = {'session': random_val}
cookies = {'admin_session': random_val}

# Send the request
r = requests.post(
    verify=False, # Bad idea in production. Always verify your certificates!
# Verify the response contains the expected data
assert r.status_code == 200
assert r.encoding == 'utf-8'
result = r.json()
assert result['result']['status'] == True
token_list = result['result']['value']['data']

# Print a list of all token serial numbers
print("**************************\nFound %s token:\n" % len(token_list))
for t in token_list:

2.4.3. Technical background

LinOTP does not check user authorization when accessing the administrative API (e.g. /admin, /system) but relies on the web server running LinOTP (as a WSGI app) to do this. The most common setup (e.g. our KeyIdentity Smart Virtual Appliance) is Apache2 web server with Digest Authentication. If the user does not identify himself correctly Apache won’t forward the request to LinOTP . If the request reaches LinOTP it is assumed the administrator authenticated correctly and his name will be extracted from a HTTP header set during Digest Authentication. Once you authenticated your browser will store your information and will not ask again for username+password typically until the next browser restart. The browser will transparently send special HTTP Digest headers with EVERY request your browser makes to the LinOTP server. This opens the door to an attack known as CSRF (Cross Site Request Forgery).

Imagine someone is able to trick your browser into calling the following URL:


Since your browser sends the HTTP digest headers in EVERY request (as long as you authenticated once after having started the browser) this request will delete your token. In general the attacker can leverage the full power of the LinOTP administrative API as long as your browser is tricked into sending his requests.

How can the browser be tricked into doing this? For example if you open a web page containing some malicious HTML such as:

<img src="https://FQDN/admin/delete?serial=LSSP0001F4E0" />

Your browser will try to display that image (which is not really an image) and execute a GET request to that URL, causing the token to be deleted.

See CSRF on Wikipedia for more information.


A common misconception is that CSRF can only be done with GET Requests. That is not true. Limiting your server to POST does not protect against CSRF.

LinOTP implements a common solution to this problem by requiring something to be sent that will NOT be automatically sent by the browser. This cannot be a cookie (because they are also sent with every request by the browser). It could be a custom HTTP header, or a custom parameter. This parameter can either contain a value that is decrypted and verified by the server (e.g. containing a timestamp that expired after a certain time) or it can simply be a value UNKNOWN to the attacker but matching a cookie. Since the attacker we are assuming only lured you onto a malicious website but has no control over your browser (and can therefore NOT see the cookies for other domains than his own) both approaches work.

LinOTP implements the second approach by using a session parameter whose value has to match the admin_session cookie.


That these variables are named session and admin_session is a little bit unfortunate and confusing (since they have nothing to do with sessions) but changing it would break backward compatibility with 3rd party software that interacts with LinOTP. Something like csrf_token would have been clearer.

2.4.4. Disabling the session parameter

You should only disable the session parameter if you fully understand what you are doing. You open your server to CSRF attacks (see the Technical background above). It can be useful during development though because it is cumbersome to set the values in every request.

In /etc/linotp2/linotp.ini add the following line to the DEFAULT section:

linotpNoSessionCheck =,

You can add any IP address or subnet in CIDR notation separated by comma.