Architecture | Usage | API | FAQ | Development
This is a work-in-progress of Clojurey implementation of RFC 6749 - The OAuth 2.0 Authorization Framework. Currently covers all scenarios described by spec:
Tokens expiration and refreshing are all in the box as well.
This implementation assumes Authorization Server and Resource Server having same source of knowledge about issued tokens and sessions. Servers might be horizontally scaled but still need to be connected to the same underlaying database (redis or sql-based). This is also why in-memory storage should be used for development only. It simply does not scale (at least not with current implementation).
All NOT RECOMMENDED points from specification have been purposely omitted for security reasons. Bearer tokens and client credentials should be passed in HTTP headers. All other ways (like query param or form fields) are ignored and will result in HTTP 401 (Unauthorized) or HTTP 403 (Forbidden) errors.
(todo) introduce JWT tokens
Store is a base abstraction of storage which, through protocol, exposes simple API to read and write entities (user, client, session, token or authorization code) that all the logic operates on. Cerber stands on a shoulders of 5 stores:
As for now, each store implements following types:
in-memory
- a store keeping its data straight in atom
. Ideal for development mode and tests.redis
- a proxy to Redis. Recommended for production mode.sql
- a proxy to relational database (eg. MySQL or PostgreSQL). Recommended for production mode.To keep maximal flexibility each store can be configured separately, eg. typical configuration might use sql
store for users and clients and redis
one for sessions / tokens / authcodes.
When speaking of configuration...
Cerber uses glorious mount to set up everything it needs to operate. Instead of creating stores by hand it's enough to describe them in EDN based configuration:
{:authcodes {:store :sql :valid-for 180}
:sessions {:store :sql :valid-for 180}
:tokens {:store :sql :valid-for 180}
:users {:store :sql
:defined []}
:clients {:store :sql
:defined []}
:landing-url "/"
:realm "http://defunkt.pl"
:endpoints {:authentication "/login"
:client-approve "/approve"
:client-refuse "/refuse"}}
:redis-spec {:spec {:host "localhost" :port 6379}}
:jdbc-pool {:init-size 1
:min-idle 1
:max-idle 4
:max-active 32
:driver-class "org.h2.Driver"
:jdbc-url "jdbc:h2:mem:testdb;MODE=MySQL;INIT=RUNSCRIPT FROM 'classpath:/db/migrations/h2/schema.sql'"}}
Words of explanation:
authcodes
auth-codes store definition, requires an auth-code life-time option (:valid-for) in seconds.sessions
sessions store definition, requires a session life-time option (:valid-for) in seconds.tokens
tokens store definition, requires a token life-time option (:valid-for) in seconds.users
users store definition.clients
oauth2 clients store definition.redis-spec
(optional) is a redis connection specification (look at carmine for more info) for redis-based stores.jdbc-pool
(optional) is a sql database pool specification (look at conman for more info) for sql-based stores.endpoints
(optional) should reflect cerber's routes to authentication and access approve/refuse endpoints.realm
(required) is a realm presented in WWW-Authenticate header in case of 401/403 http error codesCerber has its own abstraction of User (resource owner) and Client (application which requests on behalf of User). Instances of both can be predefined in configuration or created in runtime using API functions.
To configure users and/or clients as a part of environment, it's enough to list them in :defined
vector in corresponding store:
{:users {:store :in-memory
:defined [{:login "foo"
:email "foo@bar.com"
:name "Foo Bar"
:enabled? true
:password "pass"}]}
:clients {:store :in-memory
:defined [{:id "KEJ57AVGDWJA4YSEUBX3H3M2RBW53WLA"
:secret "BOQUIIPBU5LDJ5BBZMZQYZZK2KTLHLBS"
:info "Default client"
:redirects ["http://localhost"]
:grants ["authorization_code" "password"]
:scopes ["photo:read" "photo:write"]
:approved? true}]}}
Grant types allowed:
authorization_code
for Authorization Code Granttoken
for Implict Code Grantpassword
for Resource Owner Password Credentials Grantclient_credentials
for Client Credentials GrantClient scopes are configured as a vector of unique strings like "user"
, "photo:read"
or "profile:write"
which may be structurized in kind of hierarchy.
For example one can define scopes as #{"photo" "photo:read" "photo:write"}
which grants read and write permission to imaginary photo resoure and
a photo permission which is a parent of photo:read and photo:write and implicitly includes both permissions.
Cerber also normalizes scope requests, so when client asks for #{"photo" "photo:read"}
scopes, it's been simplified to #{"photo"}
only.
Note, it's perfectly valid to have an empty set of scopes as they are optional in OAuth2 spec.
When Cerber's system boots up, it tries first to load default EDN confguration which is located in cerber.edn
. Global configuration provided by this library in cerber.edn
resource
sets default values as following:
{:landing-url "http://localhost"
:realm "http://localhost"
:scopes #{}
:authcodes {:store :in-memory :valid-for 600}
:sessions {:store :in-memory :valid-for 600}
:tokens {:store :in-memory :valid-for 600}
:users {:store :in-memory}
:clients {:store :in-memory}
:endpoints {:authentication "/login"
:client-approve "/approve"
:client-refuse "/refuse"}}
No fancy stuff. No default clients, users or scopes. All stores use their in-memory implementations.
Configuration values can be easily replaced in 3 ways:
cerber.edn
in own project resourcescerber-ENV.edn
where ENV is a value of ENV
environmental variable or a JVM ENV
system property (if former was not found) or local
if no ENV was found.cerber.edn
and environment specific cerber-ENV.edn
. In this case both resources will be merged together (environment-specific entries have higher priority in case of conflicts).To complete some of OAuth2-flow actions, like web based authentication or approval dialog, Cerber tries to load HTML templates and populate embedded expressions with use of selmer. In similar way how it goes with configuration, Cerber looks for 2 HTML templates:
Both are provided by this library with a very spartan styling, just to expose the most important things inside.
Cerber OAuth2 provider defines 6 ring handlers that should be bound to specific routes. It's not done automagically. Some people love compojure some love bidi so Cerber leaves the decision in developer's hands.
Anyway, this is how bindings would look like with compojure:
(require '[cerber.handlers :as handlers])
(defroutes oauth-routes
(GET "/authorize" [] handlers/authorization-handler)
(POST "/approve" [] handlers/client-approve-handler)
(GET "/refuse" [] handlers/client-refuse-handler)
(POST "/token" [] handlers/token-handler)
(GET "/login" [] handlers/login-form-handler)
(POST "/login" [] handlers/login-submit-handler))
To recall, any change in default /login, /approve or /refuse paths should be reflected in corresponding endpoints
part of configuration.
Having OAuth Authentication Server paths set up, next step is to configure restricted resources:
(require '[cerber.oauth2.context :as ctx])
(defroutes restricted-routes
(GET "/user/info" [] (fn [req] {:status 200
:body (::ctx/user req)})))
Almost there. One missing part not mentioned yet is authorization and the way how token is validated.
All this magic happens inside handlers/wrap-authorized
handler which scans Authorization
header for a token issued by Authorization Server.
Once token is found, requestor receives set of privileges it was asking for and request is delegated down into handlers stack. Otherwise 401 Unauthorized is returned.
(require '[org.httpkit.server :as web]
[compojure.core :refer [routes wrap-routes]
[ring.middleware.defaults :refer [api-defaults wrap-defaults]]])
(def api-routes
(routes oauth-routes
(wrap-routes restricted-routes handlers/wrap-authorized))
;; final handler passed to HTTP server
(def app-handler (wrap-defaults api-routes api-defaults))
;; for HTTP-Kit
(web/run-server app-handler {:host "localhost" :port 8080}})
Having all the bits and pieces configured, it's time to run mount machinery:
(require '[cerber.oauth2.standalone.system :as system])
(system/go)
This simply starts the Cerber system by mounting all stores and populates them with configured users and clients (if any defined).
API functions are all grouped in cerber.oauth2.core
namespace and allow to manipulate with clients, users and tokens at higher level.
Full documentation can be found here.
Any errors returned in a response body are formed according to specification as following json:
{
"error": "error code",
"error_description": "human error description",
"state": "optional state"
}
or added to the error query param in case of callback requests.
Callback requests (redirects) are one of the crucial concepts of OAuth flow thus it's extremally important to have redirect URIs verified. There are several way to validate redirect URI, this implementation however goes the simplest way and does exact match which means that URI provided by client in a request MUST be exactly the same as one of URIs bound to the client during registration.
Cerber uses SQL migrations (handled by flyway) to incrementally apply changes on database schema.
All migrations live here.
You may either apply them by hand (not recommended) or use cerber.migration/migrate
which applies missing changes on database of your choice:
# for MySQL
cerber.migration> (migrate "jdbc:mysql://localhost:3306/template1?user=root&password=secret")
# for PostgreSQL
cerber.migration> (migrate "jdbc:postgresql://localhost:5432/template1?user=postgres&password=secret")
# use optional 2nd argument "info" to display migration status
cerber.migration> (migrate "jdbc:postgresql://localhost:5432/template1?user=postgres&password=secret" "info")
+----------------+-------------+---------------------+---------+
| Version | Description | Installed on | State |
+----------------+-------------+---------------------+---------+
| 20161007012907 | init schema | 2017-11-07 23:33:22 | Success |
+----------------+-------------+---------------------+---------+
Currently MySQL and Postgres are supported out of the box and recognized based on jdbc-url.
Cerber can be comfortably developed in TDD mode. Underlaying midje testing framework has been configured to watch for changes and run automatically as a boot task:
$ boot tests
Important thing is that tests go through all possible store types (including sql and redis) which means a running redis instance is expected. For sql-based stores an HSQL database is used so no other running SQL databases are required.
As usual, PRs nicely welcomed :) Be sure first that your changes pass the tests or simply add your own tests if you found no ones covering your code yet.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close