Auth handlers log in users

Your session data is:

Sessions

Gramex identifies sessions through a cookie named sid by default, and stores information against each session as a persistent key-value store. This is available as handler.session in every handler. For example, here is the contents of your handler.session variable now:

This has a randkey variable that was generated using the following code:

def store_value(handler):
    handler.session.setdefault('randkey', random.randint(0, 1000))
    return json.dumps(handler.session)

The first time a user visits the session page, it generates the randkey. The next time this is preserved.

The session cookie is:

You can change these defaults as follows:

app:
  session:
    httponly: false         # Allow JavaScript access via document.cookie
    secure: false           # Cookies can be accessed via HTTP (not just HTTPS)
    domain: .example.org    # All subdomains in .example.org can access session

You can store any variable against a session. These are stored in the secure cookie for a duration that’s controlled by the app.session.expiry configuration in gramex.yaml. Here is the default configuration:

app:
  session:
    expiry: 31                      # Session cookies expiry in days

You can override session expiry duration with a session_expiry: <days> kwarg under any auth handler. See session expiry.

The cookies are encrypted using the app.settings.cookie_secret key. Change this to a random secret value, either via gramex --settings.cookie_secret=... or in you gramex.yaml:

app:
  settings:
    cookie_secret: ...

v1.64: If you deploy multiple Gramex applications, the sid cookie used in one can conflict with the others. To avoid this, change the cookie name to something unique for each app, using app.session.cookie:

app:
  session:
    cookie: my-app-sid        # Instead of 'sid', use 'my-app-sid' as the cookie name for this app

Session data

Session data is stored in a session store that is configured as follows:

app:
  session:
    type: json                      # Type of store to use: json, sqlite, memory
    path: $GRAMEXDATA/session.json  # Path to the store (ignored for type: memory)
    expiry: 31                      # Session cookies expiry in days
    flush: 5                        # Write store to disk periodically (in seconds)
    purge: 3600                     # Delete old sessions periodically (in seconds)

Sessions can be stored in one of these type:

type Speed Persistent Distributed Version
memory faster no no all
json fast yes no all
sqlite slow yes yes 1.27
redis fast yes yes 1.36

Note: type: hdf5 is deprecated from v1.34. It is very slow and not distributed.

The default is type: json. Use this for single instances. For multiple Gramex instances, use type: redis. Here is a sample configuration:

app:
  session:
    type: redis         # Persistent multi-instance data store
    path: localhost:6379:0  # Redis server:port:db (default: localhost:6379:0)
    # You can pass more parameters to https://redis-py.readthedocs.io/en/latest/
    # by adding :key=value:key=value:... to path. For example:
    # path: localhost:6379:0:password=your-password
    expiry: 31          # Session cookies expiry in days
    flush: 5            # Not relevant for redis stores these are live
    purge: 86400        # Delete old sessions periodically (in seconds)

Before running this, you need to run the Redis database.

You can access the session data directly from the session store, or via Gramex as follows:

from gramex.handlers.basehandler import session_store_cache
# Loop through each session store -- there may be multiple stores
for store in session_store_cache.values():
    for session_id in store.store:
        print('Found session ID', session_id)

You can also access session data from inside a handler via:

for session_id in handler._session_store.store:
    print('Found session ID', session_id)

Authentication

Gramex allows users to log in using various single sign-on methods. The flow is as follows:

  1. Define a Gramex auth handler. This URL renders / redirects to a login page
  2. When the user logs in, send the credentials to the auth handler
  3. If credentials are valid, store the user details and redirect the user. Else show an error message.

After logging in, users are re-directed to the ?next= URL, else the Referer (i.e. the page from which the user visited the login page.) You can change this using the redirection configuration. For example, to use ?later= instead of ?next=, you need to do this:

url:
  login/auth:
    pattern: /$YAMLURL/login/
    handler: SimpleAuth
    kwargs:
      credentials: {alpha: alpha}
      redirect:
        query: later                  # ?later= is used for redirection
        header: Referer               # else, the Referer header
        url: /$YAMLURL/               # else redirect to this page
  app/home:
    pattern: /$YAMLURL/
    handler: ...
    kwargs:
      auth:
        login_url: /$YAMLURL/login/   # Use this as the login URL
        query: later                  # Send ?later= to the login URL

To force the user to a fixed URL after logging in, use:

url:
  login/auth:
    pattern: /$YAMLURL/login/
    handler: SimpleAuth
    kwargs:
      credentials: {alpha: alpha}
      redirect:
        url: /$YAMLURL/               # Always redirect to this page after login

Every time the user logs in, the session ID is changed to prevent session fixation.

Simple auth

This configuration creates a simple auth page:

url:
  login/simple:
    pattern: /$YAMLURL/simple   # Map this URL
    handler: SimpleAuth         # to the SimpleAuth handler
    kwargs:
      credentials:              # Specify the user IDs and passwords
        alpha: alpha            # User: alpha has password: alpha
        beta: beta              # Similarly for beta
        gamma:                  # The user gamma is defined as a mapping
          password: pwd         # One of the keys MUST be "password"
          role: user            # Additional keys can be defined
      template: $YAMLPATH/simple.html   # Optional login template

This setup is useful only for testing. It stores passwords in plain text. DO NOT USE IT IN PRODUCTION.

The user attributes in handler.current_user look like this:

{
    "id": "alpha",        // same as the user name used to log in
    "user": "alpha"       // same as id
}

For user gamma, it would have the additional attribute role specified above in the gramex.yaml:

{
    "id": "gamma",        // same as user name used to log in
    "user": "gamma",      // same as id
    "role": "user"        // any additional attributes are also added, except password
}

The template: key is optional, but you should generally associate it with a HTML login form file that requests a username and password (with an xsrf field). See login templates to learn how to create one.

Google auth

This configuration creates a Google login page:

url:
  login/google:
    pattern: /$YAMLURL/google   # Map this URL
    handler: GoogleAuth         # to the GoogleAuth handler
    kwargs:
      key: YOURKEY            # Set your app key
      secret: YOURSECRET      # Set your app secret

To get the application key and secret:

You can get access to Google APIs by specifying a scope. For example, this accesses your contacts and mails:

url:
  login/google:
    pattern: /$YAMLURL/google   # Map this URL
    handler: GoogleAuth         # to the GoogleAuth handler
    kwargs:
      key: YOURKEY            # Set your app key
      secret: YOURSECRET      # Set your app secret
      # Scope list: https://developers.google.com/identity/protocols/googlescopes
      scope:
        - https://www.googleapis.com/auth/contacts.readonly
        - https://www.googleapis.com/auth/gmail.readonly

The bearer token is available in the session key google_access_token. You can use this with ProxyHandler to access Google APIs .

Programmatically, you can pass this to any Google API with a Authorization: Bearer <google_access_token> HTTP header, or with a ?access_token=<google_access_token> query parameter. For example, this code fetches Google contacts:

@tornado.gen.coroutine
def contacts(handler):
    result = yield async_http_client.fetch(
        'https://www.google.com/m8/feeds/contacts/default/full',
        headers={'Authorization': 'Bearer ' + handler.session.get('google_access_token', '')},
    )
    raise tornado.gen.Return(result)

The user attributes in handler.current_user look like this:

{
    "id": "s.anand@gramener.com",     // email ID of the user
    "hd": "gramener.com",             // hosted domain name for G Suite accounts
    "family_name": "S",
    "name": "Anand S",
    "picture": "https://lh6.googleusercontent.com/-g6rN5UZlBjI/AAAAAAAAAAI/AAAAAAAAAfk/H5t_W1k90GQ/photo.jpg",
    "locale": "en",
    "gender": "male",
    "email": "s.anand@gramener.com",
    "link": "https://plus.google.com/105156369599800182273",
    "given_name": "Anand",
    "verified_email": true
}

SSL certificate error

Google auth and connections to HTTPS sites may fail with a CERTIFICATE_VERIFY_FAILED error. Here are possible solutions:

  1. Run conda update python to upgrade to the latest version of Python, which will use the latest ssl module.
  2. Run conda install certifi==2015.04.28 to downgrade to an older version of certifi. See this Tornado issue

Facebook auth

Available in Gramex Enterprise. This configuration creates a Facebook login page:

url:
  login/facebook:
    pattern: /$YAMLURL/facebook # Map this URL
    handler: FacebookAuth       # to the FacebookAuth handler
    kwargs:
      key: YOURKEY            # Set your app key
      secret: YOURSECRET      # Set your app secret

The user attributes in handler.current_user look like this:

{
    "id": "10154519601793455",        // Facebook ID
    "last_name": "Subramanian",
    "link": "https://www.facebook.com/app_scoped_user_id/.../",
    "name": "Anand Subramanian",
    "locale": "en_GB",
    "picture": {
        "data": {
            "height": 50,
            "is_silhouette": false,
            "url": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=...",
            "width": 50
        }
    },
    "first_name": "Anand",
    "access_token": "...",
    "session_expires": "5183999"
}

Twitter auth

Available in Gramex Enterprise. This configuration creates a Twitter login page:

url:
  login/twitter:
    pattern: /$YAMLURL/twitter  # Map this URL
    handler: TwitterAuth        # to the TwitterAuth handler
    kwargs:
      key: YOURKEY            # Set your app key
      secret: YOURSECRET      # Set your app secret

The user attributes in handler.current_user look like this:

{
    "id": "sanand0",          // Twitter ID
    "username": "sanand0",
    "follow_request_sent": false,
    "has_extended_profile": false,
    "profile_use_background_image": true,
    "default_profile_image": false,
    "suspended": false,
    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme14/bg.gif",
    "verified": false,
    "translator_type": "none",
    "profile_text_color": "333333",
    "profile_image_url_https": "https://pbs.twimg.com/profile_images/64530696/Anand2_normal.png",
    "profile_sidebar_fill_color": "EFEFEF",
    "entities": {...},
    "followers_count": 2395,
    "profile_sidebar_border_color": "EEEEEE",
    "id_str": "15265603",
    "profile_background_color": "131516",
    "needs_phone_verification": false,
    "listed_count": 100,
    "status": {...},
    "is_translation_enabled": false,
    "utc_offset": null,
    "statuses_count": 1149,
    "description": "Chief Data Scientist at Gramener",
    "friends_count": 89,
    "location": "Bangalore",
    "profile_link_color": "009999",
    "profile_image_url": "http://pbs.twimg.com/profile_images/64530696/Anand2_normal.png",
    "following": false,
    "geo_enabled": true,
    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme14/bg.gif",
    "screen_name": "sanand0",
    "lang": "en",
    "profile_background_tile": true,
    "favourites_count": 10,
    "name": "S Anand",
    "notifications": false,
    "url": "http://t.co/da5ntSjMc4",
    "created_at": "Sat Jun 28 20:11:06 +0000 2008",
    "contributors_enabled": false,
    "time_zone": null,
    "access_token": {...},
    "protected": false,
    "default_profile": false,
    "is_translator": false
}

LDAP auth

Available in Gramex Enterprise. There are 2 ways of logging into an LDAP server.

  1. Direct login with a user ID and password directly.
  2. Bind login as a “bind” user, search for an ID, and then validate the password

The first method is simpler. The second is flexible – it lets you log in with attributes other than the username. For example, you can log in with an employee ID or an email ID, etc instead of the “uid”.

Direct LDAP login

This configuration creates a direct LDAP login page:

auth/ldap:
  pattern: /$YAMLURL/ldap             # Map this URL
  handler: LDAPAuth                   # to the LDAP auth handler
  kwargs:
    template: $YAMLPATH/ldap.html   # Optional login template
    host: 10.20.30.40               # Server to connect to
    use_ssl: true                   # Whether to use SSL (LDAPS) or not
    user: 'DOMAIN\{user}'           # Check LDAP domain name with client IT team
    password: '{password}'          # This is the field name, NOT the actual password

The user: and password: configuration in gramex.yaml maps form fields to the user ID and password. Strings inside {braces} are replaced by form fields – so if the user enters admin in the user field, GRAMENER\{user} becomes GRAMENER\admin.

The optional template: should be a HTML login form that requests a username and password. (The form should have an xsrf field).

LDAP runs on port 389 and and LDAPS runs on port 636. If you have a non-standard port, specify it like port: 100.

The user attributes in handler.current_user look like this:

{
    "id": "uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org",
    "user": "uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org"
},

LDAP attributes

v1.23. You can fetch additional additional LDAP attributes like:

To fetch these, add a search: section. Below is a real-life example:

  kwargs:
    template: $YAMLPATH/ldap.html
    host: 10.20.30.40                       # Provided by client IT team
    use_ssl: true
    user: 'ICICIBANKLTD\{user}'             # Provided by client IT team
    password: '{password}'                  # This is the field name, not the actual passsword
    search:                                 # Look up user attributes by searching
        base: 'dc=ICICIBANKLTD,dc=com'      # Provided by client IT team
        filter: '(sAMAccountName={user})'   # Provided by client IT team
        user: 'ICICIBANKLTD\{sAMAccountName}'   # How the username is displayed

Bind LDAP login

This configuration creates a bind LDAP login page:

auth/ldap-bind:
  pattern: /$YAMLURL/ldap-bind            # Map this URL
  handler: LDAPAuth                       # to the LDAP auth handler
  kwargs:
    template: $YAMLPATH/ldap.html       # This has the login form
    host: ipa.demo1.freeipa.org         # Server to connect to
    use_ssl: true                       # Whether to use SSL or not
    bind:                               # Bind to the server with this ID/password
      user: 'uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org'
      password: $LDAP_PASSWORD        # Stored in a Gramex / environment variable
    search:
      base: 'dc=demo1,dc=freeipa,dc=org'  # Search within this domain
      filter: '(mail={user})'             # by email ID, rather than uid
      password: '{password}'              # Use the password field as password

This is similar to direct LDAP login, but the sequence followed is:

  1. Gramex logs in as (bind.user, bind.password).
  2. When the user submits the form, Gramex searches the LDAP server under search.base for search.filter – which becomes (mail={whatever-username-was-entered}).
  3. Finally, Gramex checks if the first returned user matches the password.

The user attributes in handler.current_user look like this:

{
    "id": "uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org",
    "user": "uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org",
    "dn": "uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org",
    "attributes": {
        "cn": ["Test Employee"],
        "objectClass": [
            "top",
            "person",
            "organizationalperson",
            "inetorgperson",
        ],
        "uidNumber": [1198600004],
        "manager": ["uid=manager,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org"],
        "krbLoginFailedCount": [0],
        "krbLastPwdChange": ["2017-06-18 11:13:37+00:00"],
        "uid": ["employee"],
        "mail": ["employee@demo1.freeipa.org"],
        "dn": "uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org",
        "loginShell": ["/bin/sh"],
        "homeDirectory": ["/home/employee"],
        "displayName": ["Test Employee"],
        "memberOf": [
            "cn=ipausers,cn=groups,cn=accounts,dc=demo1,dc=freeipa,dc=org",
            "cn=employees,cn=groups,cn=accounts,dc=demo1,dc=freeipa,dc=org"
        ],
        "givenName": ["Test"],
        "initials": ["TE"],
        ...
    }
},

Database auth

Available in Gramex Enterprise. This is the minimal configuration that lets you log in from an Excel file:

url:
  auth/db:
    pattern: /db                      # Map this URL
    handler: DBAuth                   # to the DBAuth handler
    kwargs:
      url: $YAMLPATH/auth.xlsx        # Pick up list of users from this XLSX (or CSV) file
      user:
        column: user                  # The user column in users table has the user ID
      password:
        column: password              # The users.password column has the password
      redirect:                       # After logging in, redirect the user to:
        query: next                   #      the ?next= URL
        header: Referer               # else the Referer: header (i.e. page before login)
        url: /$YAMLURL/               # else the home page of current directory

Now create an auth.xlsx with the first sheet like this:

user      password
-----     --------
alpha     alpha
beta      beta
...       ...

With this, you can log into /db as alpha and alpha, etc. It displays a minimal HTML template that asks for an ID and password, and matches it with the auth.xlsx database.

Note - Do not name user column as _user_id as it’s an internal variable.

You can configure several aspects of this flow. You can (and should) use:

Here is a more complete example:

url:
  auth/db:
  pattern: /$YAMLURL/db                 # Map this URL
  handler: DBAuth                       # to the DBAuth handler
  kwargs:
    url: sqlite:///$YAMLPATH/auth.db  # Pick up list of users from this sqlalchemy URL
    table: users                      # ... and this table (may be prefixed as schema.users)
    template: $YAMLPATH/dbauth.html   # Optional login template
    user:
      column: user                  # The users.user column is matched with
      arg: user                     # ... the ?user= argument from the form
    delay: [1, 2, 5, 10]              # Delay for failed logins
    password:
      column: password              # The users.password column is matched with
      arg: password                 # ... the ?password= argument from the form
      # You should encrypt passwords when storing them.
      # The function below specifies the encryption method.
      # Remember to change secret-key to something unique
      function: passlib.hash.sha256_crypt.encrypt(content, salt="secret-key")
      # hash: true                  # Client side encryption

You should create a HTML login form that requests a username and password (with an xsrf field). See login templates to learn how to create one.

In the gramex.yaml configuration above, the usernames and passwords are stored in the users table of the SQLite auth.db file. The user and password columns of the table map to the user and password query arguments. Here is sample code to populate it:

engine = sqlalchemy.create_engine('sqlite:///auth.db', encoding='utf-8')
engine.execute('CREATE TABLE users (user text, password text)')
engine.execute('INSERT INTO users VALUES (?, ?)', [
    ['alpha', 'alpha'],
    ['beta', 'beta'],
    # ...
])

The password supports optional encryption. Before the password is compared with the database, it is transformed via the function: provided. This function has access to 2 pre-defined variables:

  1. handler: the Handler object
  2. content: the user-provided password

For an example of how to create users in a database, see create_user_database from authutil.py.

If user login fails, the response is delayed to slow down password guessing attacks. delay: is a list of the delay durations. delay: [1, 1, 5] is the default. This means:

The user attributes in handler.current_user look like this:

{
    "id": "alpha",
    "role": "admin manager",
    "user": "alpha",
    "email": "gramex.guide+alpha@gmail.com"
}

Forgot password

DBAuth has a forgot password feature. The minimal configuration required is below:

url:
  auth/db:
    pattern: /db
    handler: DBAuth
    kwargs:
        url: sqlite:///$YAMLPATH/auth.db
        table: users
        user:
            column: user
        password:
            column: password
        forgot:
            email_from: gramex-guide-gmail    # Name of the email service to use for sending emails

Just add a forgot: section with an email_from: parameter that points to the same of an email service.

The forgot: section takes the following parameters (default values are shown):

Here is a more complete example:

kwargs:
  forgot:
    email_from: gramex-guide-auth     # Name of the email service to use for sending emails
    key: forgot                       # ?forgot= is used as the forgot password parameter
    arg: email                        # ?email= is used to submit the email ID of the user
    minutes_to_expiry: 15             # Minutes after which the link will expire
    email_column: email               # The database column that contains the email ID
    email_subject: Gramex forgot password       # Subject of the email
    email_as: "S Anand <root.node@gmail.com>"   # Emails will be sent as if from this ID
    email_text: |
        This is an email from Gramex guide.
        You clicked on the forgot password like for user {user}.
        Visit this link to reset the password: {reset_url}

Sign up

DBAuth has a new user self-service sign-up feature. It lets users enter their user ID, email and other attributes. It generates a random password and mails their user ID (using the forgot password feature).

Here is a minimal configuration. Just add a signup: true section to enable signup.

url:
  auth/db:
    pattern: /db
    handler: DBAuth
    kwargs:
      url: sqlite:///$YAMLPATH/auth.db
      table: users
      user:
        column: user
      password:
        column: password
      forgot:
        email_from: gramex-guide-gmail
      signup: true            # Enable signup

You can pass additional configurations to sign-up. For example:

signup:
  key: signup                     # ?signup= is used as the signup parameter
  template: $YAMLPATH/signup.html # Use this signup template
  columns:                        # Mapping of URL query parameters to database columns
    name: user_name               # ?name= is saved in the user_name column
    gender: user_gender           # ?gender= is saved in the user_gender column
                                  # Other than email, all other columns are ignored
  validate: app.validate(args)    # Optional validation method is passed handler.args
                                  # This may raise an Exception or return False to stop.

Integrated auth

Available in Gramex Enterprise. IntegratedAuth allows Windows domain users to log into Gramex automatically if they’ve logged into Windows.

To set this up, run Gramex on a Windows domain server. Create one if required. Then use this configuration:

auth/integrated:
    pattern: /$YAMLURL/integrated
    handler: IntegratedAuth

The user must first trust this server by enabling SSO on IE/Chrome or on Firefox. Then visiting /integrated will automatically log the user in.

The user attributes in handler.current_user look like this:

{
    "id": "EC2-175-41-170-\\Administrator", // same as domain\username
    "domain": "EC2-175-41-170-",            // Windows domain name
    "username": "Administrator",            // Windows user name
    "realm": "WIN-8S90I248M00"              // Windows hostname
}

SAML Auth

Available in Gramex Enterprise. SAML auth uses a SAML auth provided to log in. For example ADFS (Active Directory Federation Services) is a SAML auth provider. Caveats:

# Replace the below line with the relevant version for your system
pip install https://ci.appveyor.com/api/buildjobs/gt2betq01a5xogo7/artifacts/xmlsec-1.3.48.dev0-cp36-cp36m-win_amd64.whl
pip install python3-saml

This configuration enables SAML authentication.

auth/saml:
  pattern: /$YAMLURL/login
  handler: SAMLAuth
    kwargs:
      xsrf_cookies: false                   # Disable XSRF. SAML cannot be hacked via XSRF
      sp_domain: 'app.client.com'           # Public domain name of the gramex application
      https: true                           # true if app.client.com is on https
      custom_base_path: $YAMLPATH/saml/     # Path to settings.json and certs/
      lowercase_encoding: True              # True for ADFS driven SAML auth

custom_base_path points to a directory with these files:

Once configured, visit the auth handler with ?metadata added to view the service provider metadata. In the above example, this would be https://app.client.com/login?metadata.

Note: Provide the SP metadata URL to the client for relay configuration along with required claims (i.e. fields to be returned, such as email_id, username, etc.)

OAuth2

Available in Gramex Enterprise. Gramex lets you log in via any OAuth2 providers. This includes:

Here is a sample configuration for Gitlab:

url:
  auth/gitlab:
    pattern: /$YAMLURL/gitlab
    handler: OAuth2
    kwargs:
      # Create app at https://code.gramener.com/admin/applications/
      client_id: 'YOUR_APP_CLIENT_ID'
      client_secret: 'YOUR_APP_SECRET_ID'
      authorize:
        url: 'https://code.gramener.com/oauth/authorize'
      access_token:
        url: 'https://code.gramener.com/oauth/token'
        body:
          grant_type: 'authorization_code'
      user_info:
        url: 'https://code.gramener.com/api/v4/user'
        headers:
          Authorization: 'Bearer {access_token}'

It accepts the following configuration:

Azure Active Directory

Here is a sample configuration for Azure Active Directory:

url:
  auth/aad:
    pattern: /$YAMLURL/login
    handler: OAuth2
    kwargs:
      client_id: '1239bdca-----------------------------'
      client_secret: 'Pyum-----------------------------'
      authorize:
        url: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/authorize'
      access_token:
        url: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/token'
        body:
          grant_type: 'authorization_code'
          resource: https://graph.microsoft.com/
      user_info:
        url: 'https://graph.microsoft.com/v1.0/me/'
        headers:
          Authorization: 'Bearer {access_token}'
          Accept: 'application/json'
          Content-Type: 'application/json'

Email Auth

Available in Gramex Enterprise. EmailAuth allows any user with a valid email ID to log in. This is a convenient alternative to DBAuth. Users do not need to sign-up. Administrators don’t need to provision accounts. Gramex can restrict access just based on just their email ID or domain.

EmailAuth sends a one-time password (OTP) via email for users to log in. It does not store any password permanently.

This requires an email service. Here is a sample configuration:

email:
  gramex-guide-gmail:
    type: gmail                     # Type of email used is GMail
    email: gramex.guide@gmail.com   # Generic email ID used to test e-mails
    password: tlpmupxnhucitpte      # App-specific password created for Gramex guide

url:
  login:
    pattern: /$YAMLURL/login
    handler: EmailAuth                # Use email based authentication
    kwargs:
      # Required configuration
      service: gramex-guide-gmail     # Send messages using this provider
      # Send the strings below as subject and body. You can use variables
      # user=email ID, password=OTP, link=one-time login link
      subject: 'OTP for Gramex'
      body: 'The OTP for {user} is {password}. Visit {link}'
      redirect:                 # After logging in, redirect the user to:
          query: next           #      the ?next= URL
          header: Referer       # else the Referer: header (i.e. page before login)
          url: /$YAMLURL/       # else the home page of current directory

      # Optional configuration. The values shown below are the defaults
      minutes_to_expiry: 15     # Minutes after which the OTP will expire
      size: 6                   # Number of characters in the OTP
      template: auth.email.template.html    # Login template
      user:
          arg: user             # ?user= contains the user email
      password:
          arg: password         # ?password= contains the OTP

The user attributes in handler.current_user look like this:

{
    'id': 's.anand@gramener.com',         // email ID of the user
    'email': 's.anand@gramener.com',
    'hd': 'gramener.com'
}

Specific users can also be authorized. For example, this allows all users from @ibm.com and @pwc.com, as well as admin@example.org.

Note membership roles are added to other consuming endpoints in gramex.yaml, and not the EmailAuth endpoint. For more information see roles

url:
  login:
    pattern: /$YAMLURL/login
    handler: EmailAuth                # Use email based authentication
    kwargs:
      service: gramex-guide-gmail     # Send messages using this provider
      subject: 'OTP for Gramex'
      body: 'The OTP for {user} is {password}. Visit {link}'
      redirect:               # After logging in, redirect the user to:
        query: next           # the ?next= URL
        header: Referer       # else the Referer: header (i.e. page before login)
        url: /$YAMLURL/       # else the home page of current directory

  dashboard:
    pattern: ...
    handler: FileHandler     # Any valid Handler
    kwargs:
      ...
      auth:  # This defines membership roles for a particular endpoint
        membership:
          - {hd: [ibm.com, pwc.com]}
          - {email: [admin@example.org]}

The login flow is:

  1. User visits /. App shows form template asking for email (user field)
  2. User submits email. Browser redirects to POST /?user=<email>
  3. App generates a new OTP (valid for minutes_to_expiry minutes).
  4. App emails the OTP link to the user’s email. On fail, ask for email again
  5. If email was sent, app shows a message asking user to check email
  6. User clicks on email and visits link with OTP (GET /?password=<otp>)
  7. App checks if OTP is valid. If yes, logs user in and redirects
  8. On any error, shows form template with error

The template: is a Tornado template. Here is an example. When you write your own login template form, you can use these Python variables:

SMS Auth

Available in Gramex Enterprise. SMSAuth sends a one-time password (OTP) via SMS for users to log in. There is no permanent password mechanism.

This requires a working SMS service. Here is a sample configuration:

sms:
  exotel-sms:               # Create an SMS service
    type: exotel            # using Exotel
    sid: ...                # Enter your Exotel SID
    token: ...              # and your Exotel Token

url:
  login:
    pattern: /$YAMLURL/login
    handler: SMSAuth            # Use SMS based authentication
    kwargs:
      # Required configuration
      service: exotel-sms       # Send messages using this provider
      # Send this string with the %s replaced with the OTP.
      # The string should only contain one %s
      message: 'Your OTP is %s. Visit https://bit.ly/sms2auth'
      redirect:                 # After logging in, redirect the user to:
          query: next           #      the ?next= URL
          header: Referer       # else the Referer: header (i.e. page before login)
          url: /$YAMLURL/       # else the home page of current directory

      # Optional configuration. The values shown below are the defaults
      minutes_to_expiry: 15     # Minutes after which the OTP will expire
      size: 6                   # Number of characters in the OTP
      sender: gramex            # Sender ID. Works in some countries
      template: $YAMLPATH/auth.sms.template.html    # Login template
      user:
          arg: user             # ?user= contains the mobile number
      password:
          arg: password         # ?password= contains the OTP

Note: the example above relies on free credits available from Exotel. These may have run out.

The user attributes in handler.current_user look like this:

{
    'id': '+919741552552',        // Mobile number
    'user': '+919741552552'
}

The login flow is:

  1. User visits /login. App shows a template asking for phone (user field)
  2. User submits phone number. Browser posts ?user=<phone> to /login
  3. App generates a new OTP (valid for minutes_to_expiry minutes)
  4. App SMSs the OTP to the user phone number. On fail, ask for phone again
  5. App shows form template with blank OTP (password) field
  6. User submits OTP. Browser posts ?user=<phone>&password=<otp> to /login
  7. App checks if OTP is valid. If yes, logs user in and redirects
  8. If OTP is invalid, shows form template with error

The template: is a Tornado template. Here is an example. When you write your own login template form, you can use these Python variables:

Log out

This configuration creates a logout page:

auth/logout:
  pattern: /$YAMLURL/logout   # Map this URL
  handler: LogoutHandler      # to the logout handler

After logging in, users are re-directed to the ?next= URL. You can change this using the redirection configuration.

Authentication features

Login templates

Several auth mechanisms (such as SimpleAuth, LDAPAuth, DBAuth) use a template to request the user ID and password. This is a minimal template:

<form method="POST">
    {% if error %}<p>error code: {{ error['code'] }}, message: {{ error['error'] }}</p>{% end %}
    <input name="user">
    <input name="password" type="password">
    <input type="hidden" name="_xsrf" value="{{ handler.xsrf_token }}">
    <button type="submit">Submit</button>
</form>

If error is set, we display the error['code'] and error['error']. Otherwise, we have 3 input fields:

AJAX login

To using an AJAX request to log in, use this approach:

$('form').on('submit', function(e) {
  e.preventDefault()
  $('#message').append('<div>Submitting form</div>')
  $.ajax('simple', {
    method: 'POST',
    data: $('form').serialize()
  }).done(function () {
    $('#message').append('<div>Successful login</div>')
  }).fail(function (xhr, status, message) {
    $('#message').append('<div>Failed login: ' + message + '</div>')
  })
})

Note: when using AJAX, redirect: does not change the main page. The .done() method will get the contents of the redirected page as a HTML string. To redirect on success, change window.location in .done().

Login actions

When a user logs in or logs out, you can register actions as follows:

url:
  login/google:
    pattern: /$YAMLURL/google
    handler: GoogleAuth
    kwargs:
      key: YOURKEY
      secret: YOURSECRET
      action:                                     # Run multiple function on Google auth
        - function: ensure_single_session         # Logs user out of all other sessions
        - function: sys.stderr.write('Logged in via Google')      # Write to console

For example, the ldap login page is set with ensure_single_session. You can log in on multiple browsers. Every log in will log out other sessions.

You can write your own custom functions. By default, the function will be passed the handler object. You can define any other args or kwargs to pass instead. The actions will be executed in order.

When calling actions, handler.current_user will have the user object on all auth handlers and the LogoutHandler.

User attributes

All handlers store the information retrieved about the user in handler.current_user as a dictionary. This is also available, by default, as handler.session['user'].

The contents of handler.current_user varies across auth handlers. But you are guaranteed that handler.current_user['id'] is a unique user ID for that handler.

Multiple logins

Typically, users log into only one AuthHandler, like DBAuth or GoogleAuth.

Sometimes you want to log into multiple login providers – for example, to access the Google APIs. For this, you can specify a user_key: something. This stores the user object in handler.session['something'] instead of handler.session['user']. For example:

url:
  multi-google:
    pattern: /multi-google
    handler: GoogleAuth
    kwargs:
      user_key: google_user     # Store in handler.session['google_user']
      # ...

  multi-simple:
    pattern: /multi-simple
    handler: SimpleAuth
    kwargs:
      user_key: simple_user     # Store in handler.session['simple_user']
      # ...

You can access the Google user info using handler.session['google_user'] and the SimpleAuth user info using handler.session['simple_user'].

YOU CANNOT USE THIS FOR AUTHORIZATION. You’re just linking the account – not logging in with the account. Permissions are based on handler.session['user'] only. This is only to link and capture additional information about the user.

To remove this link, write Python code anywhere (e.g. FunctionHandler) to del handler.session['google_user'] or del handler.session['simple_user']. For example:

url:
  multi-unlink:
    pattern: /multi-unlink
    handler: FunctionHandler
    kwargs:
      function: '{key: handler.session.pop(key, None) for key in ["google_user", "simple_user"]}'
      redirect:
        url: /multi

Logging logins

See user logging.

Session expiry

Gramex sessions expire in 31 days by default. This is configured under app.session.expiry.

All auth handlers accept a session_expiry: <days> kwarg that changes the expiry date when the user logs in with that handler. For example:

url:
  auth/expiry:
    pattern: /$YAMLURL/expiry
    handler: SimpleAuth
    kwargs:
      session_expiry: 0.0003          # Session expires in 26 seconds
      credentials: {alpha: alpha}

This can be used to configure sessions that have a long expiry (e.g. for mobile applications) or short expiry (e.g. for secure data applications.)

To allow users to choose how long to stay logged in, use:

url:
  auth/customexpiry:
    pattern: /$YAMLURL/customexpiry
    handler: SimpleAuth
    kwargs:
      session_expiry:
        default: 4      # the default session expiry is set to 4 days
        key: remember   # When ?remember= is submitted on login
        values:         # if ?remember=...
          day: 1          # ...day, it expires in 1 day
          week: 7         # ...week, it expires in 7 days
          month: 31       # ...month, it expires in 31 days

Inactive expiry

Gramex sessions expire if the user is inactive, i.e. has not accessed Gramex, for a number of days.

By default, this is not enabled. You can add session_inactive: <days> to any Auth handler. When the user logs in with that handler, their session will expire unless they visit again within <days> days. For example:

url:
  auth/expiry:
    pattern: /$YAMLURL/expiry
    handler: SimpleAuth
    kwargs:
      session_inactive: 0.0003         # Must visit every 26 seconds
      credentials: {alpha: alpha}
  other/pages:
    ...
    kwargs:                              # Ensure that other authenticated pages
      headers:                         # also expire every 26 seconds
        Cache-Control: private, max-age=26

Change inputs

All auth handlers support a prepare: function. You can use this to modify the inputs passed by the user. For example:

The YAML configuration is:

url:
  auth/login:
    pattern: /$YAMLURL/login/
    handler: ...                # Any auth handler can be used
    kwargs:
      ...                       # Add parameters for the auth handler
      prepare: module.function(args, handler)

You can create a module.py with a function(args, handler) that modifies the arguments as required. For example:

def function(args, handler):
    if handler.request.method == 'POST':
        args['user'][0] = 'DOMAIN\\' + args['user'][0]
        args['password'][0] = decrypt(args['password'][0])
        # ... etc

The changes to the arguments will be saved in handler.args, which all auth handlers use. (NOTE: These changes need not affect handler.get_argument().)

Recaptcha

Auth handlers support a recaptcha: configuration that checks CAPTCHA validation via reCAPTCHA v3.

reCAPTCHA v3 checks if a login is legitimate without user interaction, i.e. without prompting the user to take any action. This is a frictionless mechanism. To set this up, register reCAPTCHA v3 keys here, then add this configuration:

url:
  auth/login:
    pattern: /$YAMLURL/login/
    handler: ...                # Any auth handler that supports templates
    kwargs:
      ...
      recaptcha:                # Add this section for recaptcha
        key: YOUR-RECAPTCHA-KEY
        secret: YOUR-RECAPTCHA-SECRET

If you use your own login template:, add this code at the bottom of the page:

{% if 'recaptcha' in handler.kwargs %}
  {% set recaptcha = handler.kwargs.recaptcha %}
  <script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha.key }}"></script>
  <script>
    grecaptcha.ready(function () {
      grecaptcha.execute('{{ recaptcha.key }}', { action: '{{ recaptcha.action }}' }).then(function (token) {
        document.querySelector('input[name="recaptcha"]').value = token
      })
    })
  </script>
{% end %}

Try this example, and observe the reCAPTCHA logo at the bottom-right of the screen:

Lookup attributes

Each auth handler creates a handler.session['user'] object. The keys in this object can be extended from any data source. For example, create a lookup.xlsx file with this data:

user gender role
alpha male manager
beta female employee

Add a lookup section to any auth handler and specify a url: lookup.xlsx. For example:

url:
  auth/lookup-attributes:
    pattern: /$YAMLURL/lookup-attributes
    handler: SimpleAuth
    kwargs:
      credentials:
        alpha: alpha
        beta: beta
      lookup:
        url: $YAMLPATH/lookup.xlsx      # Look for the attribute in this file
        sheet_name: Sheet1              # under this sheet
        id: user                        # under this column

Now, when the user logs in as alpha, the handler.current_user object has the gender and role attributes:

{
    "id": "alpha",
    "gender": "male",
    "role": "employee",
    // any other attributes that are already defined
}

The keys under lookup: are:

All columns in the Excel sheet are added as attributes. But if a value is NULL (not an empty string), it is ignored. In Excel, deleting a cell makes it NULL.

By default, this looks up the first sheet. You can specify an alternate sheet using sheet_name: .... For example:

        lookup:
            url: $YAMLPATH/lookup.xlsx
            sheet_name: userinfo            # Specify an alternate sheet name
            id: user

Instead of Excel files, you can use databases by specifying a SQLAlchemy URL just like for FormHandler.

    lookup:
        url: sqlite:///$YAMLPATH/database.sqlite3
        table: lookup

Automated logins

Gramex has two mechanisms to automate logins: one-time passwords and encrypted users.

OTP

Handlers support a handler.otp(expire=seconds) function. This returns a one-time password string linked to the current user. When you send a request with a X-Gramex-OTP: <otp> header or a ?gramex-otp=<otp> query parameter, that session is automatically linked to the same user.

Encrypted user

You can mimic a user by passing a X-Gramex-User HTTP header. This fetches a url as if user@example.org with manager role was logged in:

user = {'id': 'user@example.org', 'role': 'manager')
r = requests.get(url, headers={
    'X-Gramex-User': tornado.web.create_signed_value(cookie_secret, 'user', json.dumps(user))
})

cookie must be the value of app.settings.cookie_secret in gramex.yaml. You can fetch this in gramex as gramex.service.app.settings['cookie_secret'].

Authorization

By default, all handlers are publicly accessible to all.

To restrict pages to specific users, use the kwargs.auth configuration. This works on all Gramex handlers (that derive from BaseHandler).

url:
  auth/must-login:
    pattern: /$YAMLURL/must-login
    handler: FileHandler
    kwargs:
      path: $YAMLPATH/secret.html
      auth: true

auth: true just requires that you must log in. In this example, you can access this sample page “must-login” only if you are logged in.

v1.64: To protect an entire app, i.e. add auth on all pages, use:

app:
  auth: true    # All pages require login -- including CSS/images on login page!

Use with caution. This will be used as the default auth on every handler. The CSS/JS/images on your login page won’t appear unless you set auth: false on those URLs.

Note: Any auth: on an AuthHandler is ignored. (That’s asking users to log into a login page!)

You can restrict who can log in using roles or any other condition.

Login URLs

By default, this will redirect users to /login/. This is configured in the app.settings.login_url like this:

app:
  settings:
    login_url: /$YAMLURL/login/   # This is the default login URL

You need to either map /login/ to an auth handler, or change the login_url to your auth handler URL.

Each URL can choose its own login URL. For example, if you logout and visit use-simple, you will always be taken to auth/simple even though app.settings.login_url is /login/:

url:
  auth/protected-page:
    pattern: /$YAMLURL/protected-page
    handler: FileHandler
    kwargs:
      path: $YAMLPATH/protected-page.html
      auth:
        login_url: /$YAMLURL/login  # Redirect users to this login page

For AJAX requests (that send an X-Requested-With header) redirection is disabled - since AJAX cannot redirect the parent page. So:

$.ajax('protected-page')
  .done(function() { ... })     // called if protected page returns valid data
  .fail(function() { ... })     // called if auth failed.

To manually disable redirection, set login_url: false.

Roles

auth: can check for membership. For example, you can access en-male only if your gender is male and your locale is en or es. (To test it, logout and log in via Google.)

    # Add this under the kwargs: of ALL pages you want to restrict access to
    auth:
      membership:           # The following user object keys must match
        gender: male      # user.gender must be male
        locale: [en, es]  # user.locale must be en or es
        email: [..., ...] # user.email must be in in this list

If the user object has nested attributes, you can access them via .. For example, attributes.cn refers to handlers.current_user.attributes.cn.

You can specify multiple memberships that can be combined with AND or OR. This example allows (Females from @gramener.com) OR (Males with locale=en) OR (beta@example.org):

    # Add this under the kwargs: of ALL pages you want to restrict access to
    auth:
      membership:
        -                           # First rule
          gender: female                # Allow all women
          hd: [ibm.com, pwc.com]        # AND from ibm.com or pwc.com
        -                           # OR Second rule
          gender: male                  # Allow all men
          locale: [en, es]              # with user.locale as "en" or "es"
        -                           # OR Third rule
          email: beta@example.org       # Allow this user

auth: lets you define conditions. For example, you can access dotcom only if your email ends with .com, and access dotorg only if your email ends with .org.

url:
  auth/dotcom:
    pattern: /$YAMLURL/dotcom
    handler: FileHandler
    kwargs:
      path: $YAMLPATH/secret.html
      auth:
        condition:                          # Allow only if condition is true
          function: handler.current_user.email.endswith('.com')
  auth/dotorg:
    pattern: /$YAMLURL/dotorg
    handler: FileHandler
    kwargs:
      path: $YAMLPATH/secret.html
      auth:
        condition:                          # Allow only if condition is true
          function: handler.current_user.email.endswith('.org')

You can specify any function of your choice. The function must return (or yield) True to allow the user access, and False to raise a HTTP 403 error.

To repeat auth conditions across multiple handlers, see Reusing Configurations.

Protect all pages

To add access control to the entire application, use:

handlers:
  BaseHandler:
    # Protect all pages in the application. All auth: configurations allowed
    auth:
      login_url: /$YAMLPATH/login/

This is the same as adding the auth: ... to every handler in the application.

You can over-ride this auth: in a handler.

You can also apply this to specific handlers. For example, this protects all FormHandlers and FileHandlers:

handlers:
  FormHandler: {auth: true}
  FileHandler: {auth: true}

Templates for unauthorized

When a user is logged in but does not have access to the page (because of the auth condition or membership), you can display a friendly message using auth.template. Visit unauthorized-template for an example. You will see the contents of 403-template.html rendered.

url:
  auth/unauthorized-template:
    pattern: /$YAMLURL/unauthorized-template
    handler: FileHandler
    kwargs:
      path: $YAMLPATH/secret.html
      auth:
        membership:                             # Pick an unlikely condition to test template
          donkey: king                          # This condition will usually be false
        template: $YAMLPATH/403-template.html   # Render template for forbidden users