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.

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 security

The session cookie can have the following configurations:

You can change these defaults to a more secure setting as follows:

app:
  session:
    httponly: true # Allow JavaScript access via document.cookie
    secure: true # Cookies can be accessed only via HTTPS (not HTTP)
    samesite:
      Strict # Browser sends the cookie only for same-site requests.
      # Values can be Strict, Lax or None. (Case-sensitive)
    domain: example.org # All subdomains in *.example.org can access session

The cookie is stored for app.session.expiry days. Here is the default configuration:

app:
  session:
    expiry: 31 # Sessions expire after 31 days by default

Set session_expiry: false to create session cookies. Session cookies expire when the browser is closed. (Note: Gramex automatically expires session cookies on the server after 1 day, even if the browser is open for longer.)

app:
  session:
    expiry: false # Sessions expire when browser closes

You can override session expiry for individual auth handlers with a session_expiry: <days> kwarg. 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: your-cookie-secret

Session store

Sessions are stored in a session store that is configured as follows:

app:
  session:
    type: json # Type of store to use (see below)
    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          # Redis stores are live. No flush required
    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: ../ # else redirect to parent 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: ../ # 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.

Simple Auth example

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
      # Any Google OAuth2 parmeters are passed under extra_params. See:
      # https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient
      extra_params:
        prompt: select_account # Prompt to pick account every time

To get the application key and secret:

Google Auth example

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
      # Any Google OAuth2 parmeters are passed under extra_params. See:
      # https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient
      extra_params:
        prompt: select_account # Prompt to pick account every time
      # 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 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,
    "access_token": "...",
    "expires_in": 3599,
    "scope": "https://www.googleapis.com/auth/userinfo.email openid ...",
    "token_type": "Bearer",
    "id_token": "..."
}

The bearer token is available in the session key 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 <access_token> HTTP header, or with a ?access_token=<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('access_token', '')},
    )
    raise tornado.gen.Return(result)

Offline access to Google data

To perform offline actions on behalf of the user (e.g. send email, poll Google Drive, etc), you need to request offline access by adding access_type: offline under extra_params.

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
      # Any Google OAuth2 parmeters are passed under extra_params. See:
      # https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient
      extra_params:
        prompt: select_account # Prompt to pick account every time
        access_type: offline # Get a token that works offline
      # Scope list: https://developers.google.com/identity/protocols/googlescopes
      scope:
        - https://www.googleapis.com/auth/contacts.readonly
        - https://www.googleapis.com/auth/gmail.readonly

When the user logs in for the first time, Google sends a refresh token that you can access via handler.current_user.refresh_token.

If the access_token has expired, you will get a HTTP 401 Unauthorized error, with a JSON response like this:

{
  "error": {
    "code": 401,
    "message": "Request had invalid authentication credentials...",
    "status": "UNAUTHENTICATED"
  }
}

In this case, you can get a new access token using GoogleAuth.exchange_refresh_token():

@tornado.gen.coroutine
def get_contacts(handler):
    # Make an authenticated request
    url = 'https://www.google.com/m8/feeds/contacts/default/full'
    headers = {'Authorization': 'Bearer ' + handler.current_user.get('access_token', '')}
    r = requests.get(url, headers=headers)
    # If the access token has expired
    if r.status_code == 401:
        # Exchange refresh token
        yield gramex.service.url['login/google'].handler_class.exchange_refresh_token(
            handler.current_user)
        # Make the request again
        headers = {'Authorization': 'Bearer ' + handler.current_user.get('access_token', '')}
        r = requests.get(url, headers=headers)
    return r.text

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. Database auth (or DBAuth) lets users log in with a username and password from any data source. Users with email can also sign-up and reset their password.

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: . # 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.

DBAuth example

Here is a more complete example using a SQLite database:

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

DBAuth example

Other kwargs are passed to gramex.data.filter, which passes it to sqlalchemy.create_engine() for databases, gramex.cache.open() for files, or the respective plugin filters.

For example, adding:

kwargs:
  url: mysql+pymysql://localhost/test
  pool_recycle: 3600
  # ...

… is the same as setting pool_recycle=3600, i.e. sqlalchemy.create_engine('mysql+pymysql://localhost/test', pool_recycle=3600, ...)

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

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
    otp_reset: false # true clears all previous OTPs for this email
    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_body: |
      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}
    email_html: |
      <p>Hi from <a href="https://gramener.com/gramex/guide/">Gramex Guide</a>.</p>
      <p>You clicked on the forgot password like for user {user}.</p>
      <p><a href="{reset_url}">Click here</a> to reset the password.</p>

Forgot password example

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

Sign-up example

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.

First, install the onelogin SAML module:

# 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 is XSRF-proof
      sp_domain: 'app.client.com'           # Public domain name of the gramex app
      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}"

Gitlab OAuth2 example

It accepts the following configuration:

Gitlab OAuth2 example

Google OAuth2 example

Azure Active Directory

Create an OpenID application on Azure Active Directory.

Here is a sample configuration that uses the credentials from the above app:

url:
  auth/aad:
    pattern: /$YAMLURL/login
    handler: OAuth2
    kwargs:
      client_id: $AZURE_CLIENT_ID
      client_secret: $AZURE_CLIENT_SECRET
      authorize:
        url: "https://login.microsoftonline.com/{TENANT_ID}/oauth2/authorize"
        scope:
          - https://graph.microsoft.com/User.Read
        extra_params: # Optional: Lets users switch account
          prompt: select_account
      access_token:
        url: "https://login.microsoftonline.com/{TENANT_ID}/oauth2/token"
        body:
          grant_type: "authorization_code"
      user_info:
        url: "https://graph.microsoft.com/v1.0/me/"
        headers:
          Authorization: "Bearer {access_token}"
      action:
        function: myapp.store_info_in_session(handler)
      redirect:
        query: next
        url: /$YAMLURL/

Email Auth

Video

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
      from: user@example.org # Sends messages as this user
      # 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}
      html: |
        <p>The OTP for {user} is {password}.</p>
        <p><a href="{link}">Click here to log in</a></p>

      # Optional configuration. The values shown below are the defaults
      otp_reset: false # true clears all previous OTPs for this email ID
      minutes_to_expiry: 15 # Minutes after which the OTP will expire
      size: 6 # Number of characters in the OTP
      instantlogin:
        false # Fetching login link instantly logs user in
        # False is best for clients like Outlook that pre-fetch links
      user:
        arg: user # ?user= contains the user email
      password:
        arg: password # ?password= contains the OTP
      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: . # else the home page of current directory

Email auth example

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: .                # 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]}

To customize the email message that’s sent, you can change:

The email message is formatted as a Python string (i.e. {variable} is replaced with the value of variable). You can use these variables in the message:

To customize the login page, you can add a template: pointing to a Tornado template. Here is a sample. You can use these variables in the template:

Email auth template example

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: . # 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
      otp_reset: false # true clears all previous OTPs for this phone number
      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

SMS auth example

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.

In single sign-on mechanisms like Google Auth, Azure AD, SAML, etc., the user will be logged back in immediately. To avoid this, force a login prompt.

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().

AJAX auth example

Login actions

You can add custom functions to any AuthHandler. These will run when a user succesfully logs in.

For example:

url:
  login/google:
    pattern: /$YAMLURL/google
    handler: GoogleAuth
    kwargs:
      # ...
      action: # After login,
        - function: admin.send_alert_mail(handler) # Run custom functions
        - function: print(handler.current_user, 'logged in via Google')

The function can be any expression or pipeline. You can also add your custom logout actions when the user successfully logs out to LogoutHandler.

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

Failed login delay

To slow down hackers guessing passwords, add a delay: parameter under kwargs:. For example:

url:
  login/simple:
    pattern: /$YAMLURL/simple
    handler: SimpleAuth
    kwargs:
      delay: 5 # Wait 5 seconds before reporting wrong password
      credentials:
        alpha: alpha
        beta: beta

Failed login delay example

In the above example, you can log in as alpha / alpha instantaneously. But if you enter an incorrect password, it takes 5 seconds to report that.

The delay: can be specified as a number or an array of numbers. For example:

Ensure single login session

When a user logs in, you can log them out from all other sessions on any other device using the ensure_single_session login action:

url:
  login/google:
    pattern: /$YAMLURL/google
    handler: GoogleAuth
    kwargs:
      # ...
      action:
        - function: ensure_single_session # Logs user out of all other sessions

You can add the action: section under the kwargs: of any Auth handler.

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.

User store

No matter which auth handler is used, Gramex stores information about all users who ever logged in (with attributes) in storelocations.user configured as follows. The syntax is the same as for FormHandler:

storelocations:
  user:
    url: sqlite:///$GRAMEXDATA/auth.user.db
    table: user
    # Don't change these columns. Gramex needs exactly these columns.
    columns:
      key: { type: TEXT, primary_key: true }
      value: { type: TEXT }

To share user information with Gramex running on multiple servers, use a remote database with. For example, add this in your gramex.yaml:

storelocations:
  user:
    url: postgresql://$USER:$PASS@server/db
    # url: mysql+pymysql://$USER:$PASS@server/db
    # ...
    table: user

To query user data in a function when Gramex is running, use:

import gramex.data

def function(handler):
    import gramex.service
    # Access user data directly from the database
    user_data = gramex.data.filter(**gramex.service.storelocations.user, table='user')

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

Multiple login example

Logging logins

See user logging.

Session expiry

Gramex sessions expire in 31 days by default. To modify this, add session_expiry: <days> to the auth handler. For example:

url:
  auth/expiry:
    pattern: /$YAMLURL/expiry
    handler: SimpleAuth # session_expiry works on DBAuth, GoogleAuth, etc too
    kwargs:
      session_expiry: 0.0003 # Session expires in 26 seconds
      # ...

Session expiry example

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.)

You can allow users to choose how long they want to stay logged in. For example:

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 user logs in, check the value of ?remember=
        values: # Set session expiry based on the value of ?remember=
          day: 1 # If ?remember=day, session expires in 1 day
          week: 7 # If ?remember=week, session expires in 7 days
          month: 31 # If ?remember=month, session expires in 31 days
          session: false # If ?remember=session, session expires when browser closes

Remember me example

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. Enable it via session_inactive: <days> on any auth handler. When the user logs in, 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.01 # Must visit every 0.01 days, i.e. 864 seconds, or 14.4 min
      # ...
  other/pages:
    # NOTE: You must ensure that other authenticated pages are not cached beyond that duration
    kwargs:
      headers:
        Cache-Control: private, max-age=864 # 864 seconds = 0.01 days

Inactive expiry example

Change inputs

All auth handlers support a prepare: expression or pipeline. 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 or validates 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])
        if handler.request.remote_ip not in valid_list:
            raise HTTPError(403, 'Invalid IP address')

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 an input named recaptcha inside your <form>

<input type="hidden" name="recaptcha" />

… and this 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:

Recaptcha example

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

Lookup attributes example

Add Attribute Rules

Auth handlers support a rules kwarg which allows users to specify rules which can modify user attributes on the fly. For example, the following spec

url:
  auth/simple:
    pattern: /$YAMLURL/login
    handler: SimpleAuth
    kwargs:
      credentials:
        alpha:
          password: alpha
          email: jane.doe@gramener.com
          role: admin
          location: BLR
        beta:
          password: beta
          email: john.doe@gmail.com
          role: intern
          location: MUM
      rules:
        url: $YAMLPATH/rules.csv

declares two users, with three attributes each - email, role and location. The rules for modifying these attributes can be composed in a file named rules.csv as follows:

selector pattern field value
email *@gmail.com role guest
role admin location NYC

These rules can be read as follows:

  1. If the “email” attribute of a user matches the pattern *@gmail.com, then set their “role” attribute to guest, AND
  2. if the “role” attribute of a user matches the pattern “admin”, then set their “location” attribute to NYC.

In the case of the users specified above, after logging in, beta would see their role changed to “guest” from “intern”, and the user alpha would see their location changed to “NYC” from “BLR”.

Generally, for any auth handler, any existing user attribute can be used in the “selector” field, and if the value of that attribute matches the specified “pattern”, then the attribute mentioned in the “field” is modified to the “value”.

User attributes rules Example

Automated logins

Gramex has three mechanisms to automate logins: one-time passwords, API keys and encrypted users.

OTP

One-time passwords (OTP) let users to log in once. Once the user logs in, they expire.

These are useful to send one-time links. For example, to email a user a link they can log in from. (After logging in, the link expires.)

Add this code to a FunctionHandler or any Python code in a handler:

expiry = 24 * 60 * 60             # Expires in 24 hours
otp = handler.otp(expire=expiry)  # Create OTP as current user

This creates an otp string for the currently logged-in user that expires after 24 hours (or once used, whichever is earlier).

To revoke the OTP, call:

handler.revoke_otp(otp)

To create an otp for a different user, use:

expiry = 24 * 60 * 60                         # Expires in 24 hours
user = {'id': 'alpha'}                        # User to create OTP for
otp = handler.otp(expire=expiry, user=user)   # Create OTP as specified user

When a user visits any page with ?gramex-otp=<otp> added, or with a X-Gramex-OTP: <otp> header, the user is logged in for that session. handler.current_user is set to the user object.

OTP example

API key

API keys protect a URL with an access token or key. This is used to:

Create the API key for a user {'id': 'user@example.com'}, call:

user = {id: 'user@example.com'}       # Ensure there's an 'id' key
expiry = 24 * 60 * 60                 # Expires in 24 hours
key = handler.apikey(expire=expiry, user=user)  # Create key as specified user

To revoke the API key, call:

handler.revoke_apikey(key)

You can add this to a FunctionHandler or any Python code, and share it with users.

Use the API key by adding an auth: to any url:. For example:

url:
  my-api-url:
    pattern: /api/v1
    handler: FunctionHandler
    kwargs:
      function: my_api.get_users(handler)
      # Add auth: to restrict the users
      auth:
        condition:
          function: handler.current_user.id.endswith('@example.com')

You can pass the API key in 2 ways:

  1. Add the URL parameter ?gramex-key=<key>, e.g. /api/v1?gramex-key=<key>
  2. Send the HTTP header X-Gramex-Key: <key> with the URL /api/v1

In either case, handler.current_user becomes the user object (i.e. {id: 'user@example.com'}).

Gramex now behaves exactly as if that user had logged in. E.g. roles work as expected.

Try the API key example

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_secret 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'].

Distributed OTP and API keys

By default, OTPs and API keys are stored locally in a SQLite database.

If you load-balance a Gramex app across multiple servers, the OTPs and API keys created on one server won’t be shared with the other servers.

To share the keys, add an storelocations.otp configuration to gramex.yaml that points to a shared database:

storelocations:
  otp:
    url: mysql+pymysql://root@server/db
    table: otp

The url can point to any FormHandler compatible database. The table can be any new table name. Gramex tries to create it with the following columns. You can point to any existing table with these columns too:

OTP custom keys

OTPs and API keys store a user JSON object. You can pass additional keys to this object using the columns configuration.

storelocations:
  otp:
    columns:
      role: TEXT
      group: TEXT

The columns: are specified like FormHandler columns.

You can populate these columns using keyword arguments to handler.otp() and handler.api_key()

handler.otp(user={"id": "alpha"}, role="admin", group="all")
# OR...
handler.api_key(user={"id": "alpha"}, role="admin", group="all")

When the user logs in with the OTP or API key, the user object will be

{"id": "alpha", "role": "admin", "group": "all"`}

Of course, this could also be written as

handler.otp(user={"id": "alpha", "role": "admin", "group": "all"`})

But using columns like this makes it easy to query fields, e.g. to delete OTPs for all role="admin".

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.

Authorization HTTP methods

To authorize the user for some HTTP methods (e.g. POST, PUT, DELETE) but not others (e.g. GET), use this:

url:
  public-read:
    pattern: /$YAMLURL/public-read
    handler: FunctionHandler
    kwargs:
      function: f'Method = $${handler.request.method}, User = $${handler.current_user}'
      auth:
        methods: [POST, PUT, DELETE]

Any GET, OPTIONS or other HTTP requests to /public-read can be made by anyone. But POST, PUT, DELETE can only be made by logged-in users.

HTTP methods example

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.

Header validation

v1.94. To protect pages based on HTTP headers, use validate:. For example:

url:
  auth/validate:
    pattern: /$YAMLURL/validate
    handler: FileHandler
    kwargs:
      path: $YAMLPATH/secret.html
      validate: handler.request.headers['Host'] == 'example.org'

This allows access only if the Host header is example.org.

You can use any Python expression. If the expression returns a falsy value or raises an Exception, Gramex raises a HTTP 400 error.

Specify multiple conditions with a list. Gramex allows the request only if ALL conditions match. For example:

validate:
  - handler.request.headers['Host'] == 'example.org'
  - handler.request.headers['User-Agent'].startswith('Mozilla')
  - handler.current_user['id'] == 'alpha'

Customize the HTTP code and reason by specifing a dictionary with function, code, and reason. For example:

validate:
  - function: handler.request.headers['Host'] == 'example.org'
    code: 403
    reason: This app should only be hosted on example.org
  - function: handler.request.headers['User-Agent'].startswith('Mozilla')
    code: 400
    reason: Only Chrome, Edge, Firefox, and Safari are supported

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