Implementing dynamic authentication and authorization¶
While static configuration of users and permissions may work for trivial applications, you will probably find yourself wanting for more flexibility for both authentication and authorization as your application grows larger. Crossbar, the reference WAMP router implementation, supports dynamic authentication and dynamic authorization. That means that instead of a preconfigured list of users or permissions, the router itself will call named remote procedures to determine whether the credentials are valid (authentication) or whether a session has permission to register/call a procedure or subscribe/publish to a topic (authorization).
The catch-22 in this is that the WAMP client that provides these procedures has to have
permission to register these procedures. This chicken and egg problem can be solved by providing
a trusted backdoor for this particular client. In the example below, the client providing the
authenticator and authorizer services connects via port 8081 which will be only made accessible for
that particular client. Unlike the other two configured roles, the server
role has a static
authorization configuration, which is required for this to work.
version: 2
workers:
- type: router
realms:
- name: myrealm
roles:
- name: regular
authorizer: authorize
- name: admin
authorizer: authorize
- name: server
permissions:
- uri: "*"
allow: {call: true, publish: true, register: true, subscribe: true}
transports:
- type: websocket
endpoint:
type: tcp
port: 8080
auth:
ticket:
type: dynamic
authenticator: authenticate
- type: websocket
endpoint:
type: tcp
port: 8081
auth:
anonymous:
type: static
role: server
The client performing the server
role will then register the authenticate()
and
authorize()
procedures on the router:
from typing import Dict
from asphalt.core import ContainerComponent
from asphalt.wamp import CallContext, WAMPRegistry
from autobahn.wamp.exception import ApplicationError
registry = WAMPRegistry()
users = {
'joe_average': ('1234', 'regular'),
'bofh': ('B3yt&4_+', 'admin')
}
@registry.procedure
def authenticate(ctx: CallContext, realm: str, auth_id: str, details: Dict[str, Any]):
# Don't do this in real apps! This is a security hazard!
# Instead, use a password hashing algorithm like argon2, scrypt or bcrypt
user = users.get(authid)
if user:
# This applies for "ticket" authentication as configured above
password, role = user
if password == details['ticket']:
return {'authrole': role}
raise ApplicationError(ApplicationError.AUTHENTICATION_FAILED, 'Authentication failed')
@registry.procedure
def authorize(ctx: CallContext, session: Dict[str, Any], uri: str, action: str):
# Cache any positive answers
if session['authrole'] == 'regular':
# Allow regular users to call and subscribe to public.*
if action in ('call', 'subscribe') and uri.startswith('public.'):
return {'allow': True, 'cache': True}
elif session['authrole'] == 'admin':
# Allow admins to call, subscribe and publish anything anywhere
# (but not register procedures)
if action in ('call', 'subscribe', 'publish'):
return {'allow': True, 'cache': True}
return {'allow': False}
class ServerComponent(ContainerComponent):
async def start(ctx):
ctx.add_component('wamp', registry=registry)
await super().start(ctx)
For more information, see the Crossbar documentation:
Warning
At the time of this writing (2017-04-29), caching of authorizer responses has not been implemented in Crossbar. This documentation assumes that it will be present in a future release.