{"id":22901767,"url":"https://github.com/mbuczko/cerber-oauth2-provider","last_synced_at":"2026-03-16T04:03:02.589Z","repository":{"id":43243365,"uuid":"69674125","full_name":"mbuczko/cerber-oauth2-provider","owner":"mbuczko","description":"Clojure implementation of RFC 6749 OAuth 2.0 authorization framework (OAuth2 provider)","archived":false,"fork":false,"pushed_at":"2021-12-11T20:47:44.000Z","size":379,"stargazers_count":59,"open_issues_count":3,"forks_count":11,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-05-08T02:13:20.451Z","etag":null,"topics":["clojure","oauth2-provider"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mbuczko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-09-30T14:33:30.000Z","updated_at":"2025-04-22T06:43:52.000Z","dependencies_parsed_at":"2022-09-26T16:20:50.373Z","dependency_job_id":null,"html_url":"https://github.com/mbuczko/cerber-oauth2-provider","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-oauth2-provider","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-oauth2-provider/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-oauth2-provider/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-oauth2-provider/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mbuczko","download_url":"https://codeload.github.com/mbuczko/cerber-oauth2-provider/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252983766,"owners_count":21835765,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","oauth2-provider"],"created_at":"2024-12-14T01:40:49.540Z","updated_at":"2026-03-16T04:03:02.542Z","avatar_url":"https://github.com/mbuczko.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cerber - OAuth2 Provider\n\n[![Clojars Project](https://img.shields.io/clojars/v/cerber/cerber-oauth2-provider.svg)](https://clojars.org/cerber/cerber-oauth2-provider)\n\n[Architecture][arch] | [Configuration][conf] | [Usage][use] | [API][api] | [Middlewares][middlewares] | [Development][dev] | [Changelog][log]\n\nThis is a clojurey implementation of [RFC 6749 - The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749). Currently covers all scenarios described by spec:\n\n* [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1)\n* [Implict Grant](https://tools.ietf.org/html/rfc6749#section-4.2)\n* [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3)\n* [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4)\n\nTokens expiration and [refreshing](https://tools.ietf.org/html/rfc6749#section-6) are all in the box as well.\n\n## Architecture\n\nThis implementation assumes Authorization Server and Resource Server having same source of knowledge about issued tokens and sessions.\nServers 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).\n\nAll _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.\n\n_(todo)_ introduce JWT tokens\n\n### Users and clients\n\nCerber has its own abstraction of [User](./src/cerber/stores/user.clj) ([resource owner](https://tools.ietf.org/html/rfc6749#section-1.1)) and [Client](./src/cerber/stores/client.clj) (application which requests on behalf of User). Instances of both can be easily created with Cerber's API.\n\n### Stores\n\n_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:\n\n* users - keeps users details (along with encoded password)\n* clients - keeps OAuth clients data (identifiers, secrets, allowed redirect URIs and so on)\n* sessions - keeps http session data transmitted back and forth via [ring session](https://github.com/ring-clojure/ring/wiki/Sessions)\n* tokens -  generated access- and refresh tokens\n* authcodes - codes to be exchanged for tokens\n\nAs for now, each store implements following 3 types:\n\n* `:in-memory` - a store keeping its data straight in `atom`. Ideal for development mode and tests.\n* `:redis` - a proxy to Redis. Recommended for production mode.\n* `:sql` - a proxy to relational database (eg. MySQL or PostgreSQL). Recommended for production mode.\n\nTo 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.\n\nWhen speaking of configuration...\n\n## Configuration\n\n`cerber.oauth2.core` namespace is a central place which exposes all the functions required to initialize stores, users, clients and tinker with global options like realm or token/authcode/session life-times. Stores might seem to be a bit tricky to configure as they depend on underlaying storage and thus may expect additional parameters. To configure session store as redis based one, following expression should make it happen:\n\n``` clojure\n(require '[cerber.oauth2.core :as core])\n(core/create-session-store :redis {:spec {:host \"localhost\"\n                                          :port 6380}})\n```\n\nand this is how to configure SQL-based store which requires database connection passed in as a parameter:\n\n``` clojure\n(require '[cerber.oauth2.core :as core])\n(require '[conman.core :as conman])\n\n(defonce db-conn\n  (and (Class/forName \"org.postgresql.Driver\")\n       (conman/connect! {:init-size  1\n                         :min-idle   1\n                         :max-idle   4\n                         :max-active 32\n                         :jdbc-url \"jdbc:postgresql://localhost:5432/template1?user=postgres\"})))\n                         \n(core/create-session-store :sql db-conn)\n```\n\nInitialization and tear-down process can be easily handed over to glorious [mount](https://github.com/tolitius/mount):\n\n``` clojure\n(require '[mount.core :refer [defstate]])\n\n(defstate client-store\n  :start (core/create-client-store :sql db-conn)\n  :stop  (close! client-store))\n\n(defstate user-store\n  :start (core/create-user-store :sql db-conn)\n  :stop  (close! user-store))\n\n   ...and so on...\n```\n\n### Authorization Grant Types\n\nGrant types allowed:\n\n* `authorization_code` for [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1)\n* `token` for [Implict Code Grant](https://tools.ietf.org/html/rfc6749#section-4.2)\n* `password` for [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3)\n* `client_credentials` for [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4)\n\n### Scopes\n\nScopes are the OAuth way to explicitly manage the power associated with an access token. In nutshell, a scope says what type of access OAuth2 client may need to particular resource.\n\nCerber defines scopes as a set of unique strings like `user`, `photo:read` or `profile:write` which may be structurized in kind of hierarchy. For example scopes may be defined as a following: `#{\"photo:read\" \"photo:write\"}` which (when permission is granted) allows _reading_ and _writing_ to imaginary photo resoure. A `photo` scope itself is assumed to be a parent of `photo:read` and `photo:write` and implicitly includes both scopes.\n\nIn practice, scopes are auto-simplified, so when client asks for permission to `photo` and `photo:read` scopes, it's being simplified to `photo` only.\n\nNote, it's perfectly valid to have an empty set of scopes as they are optional in OAuth2 spec.\n\n### Roles and permissions\n\nAlthough User model contains `roles` field it is not interpreted in any way. It is simply returned for further processing, eg. by custom middleware.\n\nPlease take a look at [cerber-roles](https://github.com/mbuczko/cerber-roles) to make use of roles in more meaningful way.\n\n### Forms\n\nTo complete some of OAuth2-flow actions like web based authentication or asking user for approval, Cerber picks up following templates to render corresponding HTML pages:\n\n * [templates/cerber/login.html](./resources/templates/cerber/login.html) - used to render authentication form.\n * [templates/cerber/authorize.html](./resources/templates/cerber/authorize.html) - used to render an user's approval/rejection form to grant (or not) certain permissions.\n\nBoth templates are provided by this library with a very spartan styling, just to expose the most important things inside and should be replaced with own customized ones.\n\n## Usage\n\nCerber OAuth2 provider defines 7 [ring handlers](https://github.com/ring-clojure/ring/wiki/Concepts) that should be bound to specific routes. It's not done automagically. Some people love [compojure](https://github.com/weavejester/compojure) some love [bidi](https://github.com/juxt/bidi) so Cerber leaves the decision in developer's hands.\n\nAnyway, this is how bindings would look like with compojure:\n\n``` clojure\n(require '[cerber.handlers :as handlers])\n\n(defroutes oauth-routes\n  (GET  \"/authorize\" [] handlers/authorization-handler)\n  (POST \"/approve\"   [] handlers/client-approve-handler)\n  (GET  \"/refuse\"    [] handlers/client-refuse-handler)\n  (POST \"/token\"     [] handlers/token-handler)\n  (GET  \"/login\"     [] handlers/login-form-handler)\n  (POST \"/login\"     [] handlers/login-submit-handler)\n  (GET  \"/logout\"    [] handlers/logout-handler))\n```\n\nHaving OAuth paths set up, next step is to configure routes to protected resources (assuming here a user's details as such a one):\n\n``` clojure\n(require '[cerber.oauth2.context :as ctx])\n\n(defroutes authorized-routes\n  (GET \"/user/info\" [] (fn [req] {:status 200\n                                  :body (::ctx/user req)})))\n```\nAlmost there. One missing part not mentioned yet is authorization and the way how token is validated.\n\nAll the magic happens inside `wrap-authorized` middleware which examines both request Cookie (for session identifier) and `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.\n\n``` clojure\n(require '[org.httpkit.server :as web]\n          [cerber.handlers :refer [wrap-authorized]]\n          [compojure.core :refer [routes wrap-routes]\n          [ring.middleware.defaults :refer [api-defaults wrap-defaults]]\n          [ring.middleware.format :refer [wrap-restful-format]]])\n\n(def api-routes\n  (routes oauth-routes\n          (-\u003e authorized-routes\n              (wrap-routes wrap-restful-format :formats [:json-kw])\n              (wrap-routes wrap-authorized)))\n\n;; final handler passed to HTTP server (HTTP-Kit here)\n(web/run-server (wrap-defaults api-routes api-defaults) {:host \"localhost\" :port 8080}})\n```\n\n## API\n\nAPI functions are all grouped in `cerber.oauth2.core` namespace based on what entity they deal with.\n\n### stores\n\n`(create-user-store [type config])`\n\n`(create-client-store [type config])`\n\n`(create-session-store [type config])`\n\n`(create-authcode-store [type config])`\n\n`(create-token-store [type config])`\n\nFunctions to initialize empty store of given type - :in-memory, :sql or :redis one. Redis-based store expects redis connection spec\npassed in a `config` parameter whereas SQL-based one requires an initialized database connection.\n\n### clients\n\n`(create-client [grants redirects \u0026 {:keys [info scopes enabled? approved? id secret]}])`\n\nUsed to create new OAuth client, where:\n- `grants` is vector of allowed grant types: \"authorization\\_code\", \"token\", \"password\", \"client\\_credentials\". At least one grant needs to be provided.\n- `redirects` is a validated vector of approved redirect-uris. Note that for security reasons redirect-uri passed along with token request should match one of these entries.\n- `info` is a non-validated info string (typically client's app name or URL to client's homepage)\n- `scopes` is vector of OAuth scopes that client may request an access to\n- `enabled?` decides whether client should be auto-enabled or not. It's false by default which means client is not able to request for tokens\n- `approved?` decides whether client should be auto-approved or not. It's false by default which means that client needs user's approval when requesting access to protected resource\n- `id` - optional client-id (must be unique), auto-generated if none provided\n- `secret` - optional client-secret (must be hard to guess), auto-generated if none provided\n\nExample:\n\n```clojure\n(require '[cerber.oauth2.core :as c])\n\n(c/create-client [\"authorization_code\" \"password\"]\n                 [\"http://defunkt.pl/callback\"]\n                 :info \"http://defunkt.pl\"\n                 :scopes [\"photo:read\" \"photo:list\"]\n                 :enabled? true\n                 :approved? false)\n```\n\nEach generated client has its own random client-id and a secret which both are used in OAuth flow.\nImportant thing is to keep the secret codes _really_ secret! Both client-id and secret authorize\nclient instance and it might be harmful to let attacker know what's your client's secret code is.\n\n`(find-client [client-id])`\n\nLooks up for client with given identifier.\n\n`(delete-client [client])`\n\nRemoves client from store. Note that together with client all its access- and refresh-tokens are revoked as well.\n\n`(disable-client [client-id])`\n\n`(enable-client [client-id])`\n\nDisables or enables client with given identifier. Disabled client is no longer able to receive access/refresh-tokens nor operate on behalf of user in any other way.\n\n### users\n\n`(create-user [login password \u0026 {:keys [name email roles enabled?]}])`\n\nCreates new user with following details:\n\n- `:login` is a user's login identifier\n- `:password` is a user's password\n- `:name` is a user's description (like full name)\n- `:email` is a user's email\n- `:roles` set of optional roles\n- `:enabled?` indicates whether user should be enabled. User is enabled by default unless `enabled?` states otherwise.\n\n`(find-user [login])`\n\nLooks up for a user with given login.\n\n`(delete-user [login])`\n\nRemoves from store user with given login.\n\n`(disable-user [login])`\n\n`(enable-user [login])`\n\nDisables or enables user with given given login. Disabled user is no longer able to authenticate and all authorization attempts fail immediately.\n\n`(init-users [users])`\n`(init-clients [clients])`\n\nInitializes users- and clients-store with predefined collection of users/clients:\n\n```clojure\n(require '[cerber.oauth2.core :as c])\n\n(c/init-users [{:login \"admin\"\n                :email \"admin@bar.com\"\n                :name \"Admin\"\n                :enabled? true\n                :password \"secret\"\n                :roles #{:user/admin}}\n               {:login \"foo\"\n                :email \"foo@bar.com\"\n                :name \"Foo Bar\"\n                :enabled? true\n                :password \"pass\"\n                :roles #{:user/all}}])\n```\n\n\n### tokens\n\n`(find-access-token [secret])`\n\nReturns an access token bound to given secret.\n\n`(revoke-access-token [secret])`\n\nRevokes given access-token.\n\n`(find-refresh-tokens [client-id])`\n\n`(find-refresh-tokens [client-id login])`\n\nReturns collection of refresh-tokens for given client (and user optionally).\n\n`(revoke-client-tokens [client-id])`\n\n`(revoke-client-tokens [client-id login])`\n\nRevokes all access- and refresh-tokens bound with given client (and user optionally).\n\n`(regenerate-tokens [client-id login scope])`\n\nRefreshes tokens for given client-user pair. Revokes and overrides existing tokens, if any exist.\n\n### global options\n\n`(set-token-valid-for! valid-for)`\n\nSets up a token time-to-live (TTL) which essentially says how long OAuth2 tokens are valid.\n\n`(set-authcode-valid-for! valid-for)`\n\nSets up an authcode time-to-live (TTL) which essentially says how long authcodes are valid.\n\n`(set-session-valid-for! valid-for)`\n\nSets up a session time-to-live (TTL) which essentially says how long sessions are valid.\n\n`(set-landing-url! url)`\n\nSets up a location that browser should redirect to in order to authenticate a user.\n\n`(set-realm! realm)`\n\nSets up a realm presented in WWW-Authenticate header in case of 401/403 http error codes.\n\n`(set-authentication-url! url)`\n\nSets up an OAuth2 authentication URL (\"/login\" by default).\n\n`(set-unauthorized-url! url)`\n\nSets up location where browser redirects in case of `HTTP 401 Unauthorized` (\"/login\" by default).\n\n### errors\n\nAny errors returned in a response body are formed according to specification as following json:\n\n``` json\n{\n  \"error\": \"error code\",\n  \"error_description\": \"human error description\",\n  \"state\": \"optional state\"\n}\n```\n\n## Middlewares\n\nCerber exposes 2 middlewares in `cerber.handlers` namespace:\n\n`wrap-authorized`\n\nThis one, based on cookie or bearer token conveyed in a request, sets up a context where a subject (authorized user) and OAuth2 client information is stored for a request time-life.\nUnauthorized requests result in `HTTP 401 Unauthorized` (in case of invalid token) or redirection to login page (in case of cookie based request).\n\n`wrap-maybe-authorized`\n\nSame as `wrap-authorized` but does no redirection or `HTTP 401 Unauthorized` responses in case of unauthorized requests. In this case a request context is simply not created and no user/client information is available.\n\n## Development\n\nUnderlaying [midje](https://github.com/marick/Midje) testing framework has been configured to watch for changes and run corresponding tests after each change:\n\n``` shell\n$ boot tests\n```\n\nThis library has also built-in [standalone testing server](./src/cerber/oauth2/standalone/server.clj) available in `cerber.oauth2.standalone.server` namespace. All it needs to start up is initialized with mount-based restartable system:\n\n``` clojure\n(require '[cerber.oauth2.standalone.system :as system])\n\n;; start server\n(system/go)\n\n;; stops server\n(system/stop)\n\n;; restart server\n(system/reset)\n```\n\nAny ideas or bugfixes? PRs nicely welcomed. Be sure that your changes pass all the tests or simply add your own test suites if none covers your code yet.\n\n## Changelog\n\n- `v2.0.0` : internal API reworked. roles are represented by keywords now (instead of strings).\n- `v1.1.0` : `wrap-authorized` handler no longer wraps response in `wrap-restful-format` middleware, so response is not returned as json now. from now on, it' up to developer what format response will be transformed to.\n\n[arch]: #architecture\n[conf]: #configuration\n[use]: #usage\n[api]: #api\n[middlewares]: #middlewares\n[dev]: #development\n[log]: #changelog\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmbuczko%2Fcerber-oauth2-provider","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmbuczko%2Fcerber-oauth2-provider","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmbuczko%2Fcerber-oauth2-provider/lists"}