Porter is an effort to ease dealing with user accounts and its related functions: authentication and authorization. The root idea is not only to have a library to help implementing this, but rather to have a standalone service that you can simply use. So applications no matter what language written in won't need to implement this again. Instead they would send requests to the porter application.
Porter is divided into three parts:
api
: This module is a library with the least feasible dependencies. There are two dependencies for adding password encryption features: bcrypt and scrypt. It can be used for embedding porter in your application and provides a simple http client for connecting to external porter instance. There are additional optional dependencies that are only needed for certain features (e.g. the http client).app
: This module provides the porter application based on akka and spray. Therefore it is possible to connect to this application via akka remoting. Besides this, there is a http rest endpoint for clients that are not build with akka and a simple telnet interface for managing the application.openid
: OpenId is a specification to allow decentralized authentication. This module implements an OpenID Provider according to OpenId 2.0 (not everything is implemented yet, e.g. Relying Parties are not discovered as written in chapter 13 and OpenId 1.1 compatibility is not done). It also provides some very basic html pages to register and manage your account.
The dist
module packs all three together and creates a deployable
artifact -- a zip file containing the complete application.
Use sbt to build:
sbt dist
compiles everything and puts the final zip file in dist/target
.
The realm here is a name for a distinct set of accounts and groups. An account (or group) does always belong to exactly one realm. An account has a unique name, a set of secrets (e.g. passwords) and a set of properties. The group also has a unique name and a set of rules, which are either permissions or revocations. An account can be member of several groups. Accounts are needed for authentication and accounts + groups are needed for authorization.
When authenticating, a given Set[Credentials]
is validated against
the secrets of an account. The credentials must supply enough
information to lookup an account. Then the Validator
s are consulted
which produces an AuthResult
object. Each validator can "vote"
whether this credentials are correct or not. Or it may simply not
vote, if it cannot cope with the provided credentials.
Whether the AuthResult
denotes a successful authentication or not is
up to client code. For example, some applications may want to check
certain properties of an account or may require the client to supply
at least two valid credentials. The Decider
, which is a function
AuthResult => Boolean
, is meant to decide whether an AuthResult
presents a successful authentication. There are two implementations
provided. The default one DefaultPasswordVote
returns true
if
there is at least one successful vote for the standard Password
and the account is not disabled.
Authorization is based on Permission
s that can be associated to an
account via groups. Accounts have the permissions of all its groups
combined. When receiving a permission, it can be verified whether it
is included in the permissions owned by the account.
A permission in porter is defined by the trait Permission
, which
only defines one method:
def implies(other: Permission): Boolean
This looks the same as in most other permission classes, for example
java.security.Permission
. It performs a simple subset check, to
determine whether the given permission is included or "implied" by
this
permission. Each permission is required to have a string
representation. This is needed to pass permissions around the network
and to easily specify them. The DefaultPermission
is taken from the
shiro's wildcard permission. The
permission string consists of parts that are separated by colon
":
". Each part can consist of multiple elements, which are separated
by comma ",
". For example:
part1:part2e1,part2e2,part2e3:part3:part4:*:part5
The meaning of each part is completely up to the application. The
implies
works by comparing the parts of two permissions from left to
right. Then p1 implies p2
, if the list of elements of a part of p1
is a super set of the elements of the corresponding part of p2
. The
wild card *
is used as a maximum value. Any list of elements is
a subset of *
.
This makes it possible to define permission sets, like for example
file:open:*
. Then file:open:one.pdf
is implied by file:open:*
,
since the *
wild card matches any elements (and also any other
remaining parts).
The ResourcePermission
is a special sub class of DefaultPermission
and reuses the notion of parts. The ResourcePermission
must consist
of exactly three parts:
domain:actions:uri-patterns
The domain part must be "resource"
. The actions can be some
arbitary action strings, like read
, write
or delete
. This is
again up to the application. For example: resource:read:/folder1/**
could be used to grant the read
permission for all resources below
/folder1/
. It is possible to specify more uripatterns separated by
comma.
Again wildcards can be used. The ResourcePermission
supports *
,
**
and ?
wild cards.
While the wild cards allow you to define whole sets of permissions
quite easily, it is still hard to define certain sets. For example:
allow resource:/main/**
but not resource:/main/internal/**
. If
this is needed, a Revocation
can be used to selectively revoke
permissions.
A revocation is the negation of a permission and is written with a
"!
" in front of a normal permission definition. For example,
!resource:read:/main/internal/**
means to revoke the permission
resource:read:/main/internal/**
. With this you can first grant a
broader set of permissions and revoke certain subsets again. The rules
resource:read:/main/**
!resource:read:/main/internal/**
would grant read access to all resources below /main/
but access to
resources below /main/internal
is not allowed.
Porter will create two sets, one for all permissions and one for all revocations. A permission that is in the first set and not in the second is considered to be granted.
There are two main usage scenarios:
- External porter server
- Embed porter in your application
The advantage of an external porter application is that many applications can use it for authentication, enabling single-sign-on functionality.
You can connect via akka's remoting feature, by looking up the porter actor or use the provided http rest endpoint. The most decoupled way is to authenticate via OpenId.
Porter needs to be installed first. The default configuration expects
a running MongoDB server at its default
endpoint, so install mongodb first. Then unzip the porter archive
somewhere, check the configuration file porter.conf
inside the etc
directory and start porter using the script in bin
.
This starts by default all interfaces:
- akka remote on port 4554
- http on port 6789
- telnet on port 2345
- openid on port 8888
For configuring, please look at the provided porter.conf
file in the
etc/
directory and change it accordingly. Here is a short overview
of the settings:
The first part configures akka to enable the remoting feature and enables debugging through slf4j.
akka {
actor {
provider = "akka.remote.RemoteActorRefProvider"
}
remote {
enabled-transports = [ "akka.remote.netty.tcp" ]
netty.tcp {
hostname = "127.0.0.1"
port = 4554
}
}
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
}
Then the porter setting follows. At first there is a list of
Validator
s. Those are responsible for validating credentials against
a given account. By default, there are three defined:
PasswordValidator
validates passwordsDigestValidator
validates http digest authenticatorsDerivedValidator
validates the specialDerivedCredentials
which can be created out of any other given secret. The intended use is to create an authenticator to be stored in cookies
You need to define the FQCN and its constructor arguments.
The store configuration defines all stores, read-only and mutable ones by their FQCN. For mutable stores, you can additionally configure for which realms they should be used. Maybe you want all accounts of one realm to be saved in store A and others in store B. If nothing is specified, the first mutable store will be used.
The last part to configure are PermissionFactory
s. They can create
Permission
objects from strings. If you need other permission
implementations than provided, you could define them here.
All remaining configuration regards the interfaces: telnet, http and openid.
The telnet interface is for administrative tasks. The port should be protected, of course.
In a terminal, type
$telnet localhost 2345
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Welcome to porter 0.1.0
Type 'help' for a list of available commands.
porter>
The help
command lists all available commands.
This interface is intended for clients that are not built with
akka (and maybe can't use porter-api
artefact). This http server accepts json formatted http requests and
responds with json.
For example, an authentication request is as follow:
curl -i -X POST -d '{ "realm": "default", "creds": [{ "accountName": "eike", "password": "test"}] }' -H "Content-Type: application/json" http://localhost:6789/api/authc
For convenience, there is a simple http client class
porter.client.http.PorterHttp
that does the (un)marshalling for you.
If your application is build on akka, just lookup the remote porter actor.
The Porter
extension can help with this:
Porter(system).select()
The porter messages are defined in porter.client.messages
, authenticating a
user could be as follows:
import porter.client.messages._
val porter = Porter(system).select()
(porter ? Authenticate("realm", Set(PasswordCredentials("eike", "test"))).mapTo[AuthenticateResp]
The PorterUtil
object defines a set of utility functions around the
basic porter messages. The authenticationFuture
in there also
updates account properties based on the result.
When using the spray toolkit, the
PorterDirectives
trait may be useful.
OpenId is
the most standardized form of authenticating with porter. The downside
is, that it is much more complicated than the http
interface, for
example, because it enables arbitrary parties to exchange
authentication information securely. If openid is enabled, porter is
an OpenId Provider and clients need to follow the OpenId spec when
authenticating users. That also enables you to use your own porter
instance to authenticate with any website that supports OpenId
authentication.
How to authenticate using the OpenId scheme is out of scope for this
document. The OpenId identifier is by default the url to the porter
openid endpoint with the account name appended, like
https://mydomain.com/<account>
. This can be changed via the
configuration.
Authorization cannot be performed using openid.
The openid
module provides a simple web page to manage your account. It
allows to change some basic properties, the password and to upload an avatar
image. The avatar image can be requested from the url, which is by default
/openid/avatar/<account name>?size=80
. The additional size attribtue can
be used to scale the image. If no avatar image is available for an account
a random image is generated, much like an identicon
but adding some math functions to emulate a harmonograph.
When authenticating via OpenId, you need to know your OpenId identifier. With
default settings, this is the url http://localhost:8888/<accountname>
. There
is one little quirk: The accountname
must be different from "openid", since
the path prefix /openid
is reserved for the OpenId endpoint. All pages are
provided via mustache templates. You can create your own in a sub directory
inside the installation directory which is static
by default, but can be
overriden in the settings. If you only want to override some css, put a file
custom.css
in that directory as this will be picked up before the default
custom.css
.
When your application is build with akka, you can
pull in the app
module and create new porter actor. Otherwise, pull
in the api
dependency and implement the needed glue code for your
application.
The akka extension porter.app.akka.Porter
can be
used to create a new porter actor:
class MyActor extends akka.actor.Actor {
val porter = Porter(context.system).createPorter(context)
}
This will use the default configuration to create the actor. Then you
can pass messages from porter.client.Messages
.
When not using akka, there is the trait
porter.api.auth.Porter
that combines all porter features. You need
to implement the missing parts, by at least providing a Store
that
contains account information. Optionally, you can provide a mutable
store, different set of validators or a custom permission factory by
overriding corresponding methods:
class MyPorter extends Porter {
def store: Store = ???
override def validators: Iterable[Validator] = super.validators
override def mutableStore(realm: Ident): Option[MutableStore] = super.mutableStore(realm)
override def permissionFactory: PermissionFactory = super.permissionFactory
}
The package porter.store
contains Store
skelletons and an
implementation based on java properties files.