├── src └── bolt │ ├── storage │ ├── sql_storage.clj │ ├── protocols.clj │ ├── atom_storage.clj │ └── file_storage.clj │ ├── oauth │ ├── registry │ │ ├── protocols.clj │ │ └── ref_backed_registry.clj │ ├── authorization.clj │ ├── encoding.clj │ ├── registry.clj │ ├── server │ │ ├── logout.clj │ │ └── server.clj │ ├── resource.clj │ ├── client.clj │ └── client │ │ └── web_client.clj │ ├── restricted.clj │ ├── user │ ├── buddy_user_authenticator.clj │ ├── email_user_store.clj │ ├── totp.clj │ ├── protocols.clj │ ├── login.clj │ ├── reset_password.clj │ └── signup.clj │ ├── authentication │ └── protocols.clj │ ├── storage.clj │ ├── session │ ├── protocols.clj │ └── cookie_session_store.clj │ ├── token_store │ ├── protocols.clj │ └── atom_backed_store.clj │ ├── session.clj │ ├── token_store.clj │ ├── authorization.clj │ ├── util.clj │ ├── authentication.clj │ └── user.clj ├── dev ├── resources │ ├── highlight.zip │ ├── templates │ │ ├── example1 │ │ │ ├── test.html.mustache │ │ │ ├── dialog.html.mustache │ │ │ ├── protected.html.mustache │ │ │ ├── index.html.mustache │ │ │ └── page.html.mustache │ │ └── page.html.mustache │ ├── public │ │ ├── img │ │ │ ├── favicon.ico │ │ │ ├── juxt-logo.png │ │ │ └── juxt-logo.svg │ │ ├── fonts │ │ │ ├── latin.woff2 │ │ │ ├── SwedenSans.otf │ │ │ ├── latin-ext.woff2 │ │ │ ├── RochesterYada.woff2 │ │ │ └── SwedenSansBold.otf │ │ ├── css │ │ │ ├── example-style.css │ │ │ ├── fonts.css │ │ │ └── style.css │ │ └── js │ │ │ ├── examples.js │ │ │ └── tests.js │ ├── shim.html │ ├── logback.xml │ └── user-guide.md └── src │ ├── user.clj │ ├── bolt │ └── dev │ │ ├── database.clj │ │ ├── main.clj │ │ ├── website.clj │ │ ├── view.clj │ │ ├── login_form.clj │ │ ├── example1.clj │ │ ├── user_guide.clj │ │ └── system.clj │ └── dev.clj ├── .gitignore ├── LICENSE ├── test └── bolt │ ├── roles_test.clj │ └── atom_backed_store_test.clj ├── project.clj └── README.md /src/bolt/storage/sql_storage.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.storage.sql-storage) 2 | -------------------------------------------------------------------------------- /dev/resources/highlight.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/highlight.zip -------------------------------------------------------------------------------- /dev/resources/templates/example1/test.html.mustache: -------------------------------------------------------------------------------- 1 | 2 |

Hello World!

3 | 4 | -------------------------------------------------------------------------------- /dev/resources/public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/img/favicon.ico -------------------------------------------------------------------------------- /dev/resources/public/fonts/latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/fonts/latin.woff2 -------------------------------------------------------------------------------- /dev/resources/public/img/juxt-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/img/juxt-logo.png -------------------------------------------------------------------------------- /dev/resources/public/fonts/SwedenSans.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/fonts/SwedenSans.otf -------------------------------------------------------------------------------- /dev/resources/public/fonts/latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/fonts/latin-ext.woff2 -------------------------------------------------------------------------------- /dev/resources/public/fonts/RochesterYada.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/fonts/RochesterYada.woff2 -------------------------------------------------------------------------------- /dev/resources/public/fonts/SwedenSansBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juxt/bolt/HEAD/dev/resources/public/fonts/SwedenSansBold.otf -------------------------------------------------------------------------------- /src/bolt/oauth/registry/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.oauth.registry.protocols) 2 | 3 | (defprotocol ClientRegistry 4 | (register-client [_ properties]) 5 | (lookup-client [_ client-id])) 6 | -------------------------------------------------------------------------------- /dev/resources/templates/example1/dialog.html.mustache: -------------------------------------------------------------------------------- 1 |
2 | 6 | 9 |
10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | /contrib/bootstrap-login-form/.nrepl-port 11 | /contrib/bootstrap-login-form/target/repl-port 12 | /contrib/bootstrap-login-form/target/stale/extract-native.dependencies 13 | *.DS_Store 14 | notes.org 15 | todo.org -------------------------------------------------------------------------------- /src/bolt/oauth/authorization.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.oauth.authorization 4 | (:require 5 | [bolt.authentication :refer (authenticate)]) 6 | ) 7 | 8 | (defn scope-authorized? [authenticator req scope] 9 | (let [creds (authenticate authenticator req)] 10 | (contains? (:bolt/scopes creds) scope))) 11 | -------------------------------------------------------------------------------- /src/bolt/storage/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.storage.protocols) 2 | 3 | (defprotocol Storage 4 | "" 5 | (find-object [_ qualifier] "Find objects matching the qualifier") 6 | (store-object! [_ object] "Store object (or objects, if sequence) in store") 7 | (delete-object! [_ qualifier] "Delete objects matching the qualifier")) 8 | 9 | (defprotocol StorageWithExpiry) 10 | -------------------------------------------------------------------------------- /dev/resources/shim.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /dev/src/user.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. 2 | 3 | (ns user) 4 | 5 | ;; This is an old trick from Pedestal. When system.clj doesn't compile, 6 | ;; it can prevent the REPL from starting, which makes debugging very 7 | ;; difficult. This extra step ensures the REPL starts, no matter what. 8 | 9 | (defn dev 10 | [] 11 | (require 'dev) 12 | (in-ns 'dev)) 13 | 14 | 15 | (defn go 16 | [] 17 | (println "Don't you mean (dev) ?")) 18 | -------------------------------------------------------------------------------- /src/bolt/restricted.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.restricted) 4 | 5 | (defprotocol Restricted 6 | (authorized? [_ credentials])) 7 | 8 | (extend-protocol Restricted 9 | clojure.lang.Fn 10 | ;; Unrestricted functions are not wrapped in a record, but must be able to 11 | ;; give an answer on a call to authorized? above. 12 | (authorized? [_ credentials] true) 13 | nil 14 | (authorized? [_ credentials] nil)) 15 | -------------------------------------------------------------------------------- /dev/resources/templates/example1/protected.html.mustache: -------------------------------------------------------------------------------- 1 |
2 | 9 | 12 |
13 | -------------------------------------------------------------------------------- /dev/resources/templates/example1/index.html.mustache: -------------------------------------------------------------------------------- 1 |
2 | 13 | 16 |
17 | -------------------------------------------------------------------------------- /src/bolt/user/buddy_user_authenticator.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.user.buddy-user-authenticator 2 | (:require 3 | [buddy.hashers :as hs] 4 | [bolt.user.protocols :refer (UserAuthenticator UserPasswordHasher)])) 5 | 6 | (defrecord BuddyUserAuthenticator [] 7 | UserAuthenticator 8 | (authenticate-user [_ user credential] 9 | (hs/check (:password credential) (:password user))) 10 | 11 | UserPasswordHasher 12 | (hash-password [_ password] 13 | (hs/encrypt password))) 14 | 15 | (defn new-buddy-user-authenticator [& {:as opts}] 16 | (->> opts 17 | (merge {}) 18 | map->BuddyUserAuthenticator)) 19 | -------------------------------------------------------------------------------- /src/bolt/authentication/protocols.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.authentication.protocols) 4 | 5 | (defprotocol RequestAuthenticator 6 | (authenticate [_ request] 7 | "Return (as a map) any credentials that can be determined from the 8 | given Ring request")) 9 | 10 | (extend-protocol RequestAuthenticator 11 | nil 12 | (authenticate [_ request] nil)) 13 | 14 | (defprotocol AuthenticationHandshake 15 | (initiate-authentication-handshake [_ request] 16 | "Return a Ring response that redirects the user-agent into an 17 | interaction to establish its authenticity")) 18 | -------------------------------------------------------------------------------- /src/bolt/oauth/encoding.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.oauth.encoding 2 | (:require 3 | [ring.util.codec :refer (url-encode url-decode)] 4 | [clojure.string :as str] 5 | [plumbing.core :refer (?>>)])) 6 | 7 | (defn encode-scope [scopes] 8 | (->> 9 | scopes 10 | (?>> (keyword? (first scopes)) (map #(apply str (interpose ":" (remove nil? ((juxt namespace name) %)))))) 11 | (interpose " ") 12 | (apply str) 13 | url-encode)) 14 | 15 | (defn decode-scope [s should-be-keyword?] 16 | (->> (str/split (url-decode (or s "")) #"\s") 17 | (remove empty?) 18 | (?>> should-be-keyword? (map (fn [x] (apply keyword (str/split x #":"))))) 19 | set)) 20 | -------------------------------------------------------------------------------- /src/bolt/storage.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.storage 4 | (:require 5 | [schema.core :as s] 6 | [bolt.storage.protocols :as p])) 7 | 8 | ;; Storage API 9 | 10 | (s/defschema Obj {s/Keyword s/Str}) 11 | 12 | (s/defn find-object :- Obj 13 | [component :- (s/protocol p/Storage) 14 | qualifier :- s/Any] 15 | (p/find-object component qualifier)) 16 | 17 | (s/defn store-object! :- nil 18 | [component :- (s/protocol p/Storage) 19 | obj :- Obj] 20 | (p/store-object! component obj)) 21 | 22 | (s/defn delete-object! :- Obj 23 | [component :- (s/protocol p/Storage) 24 | qualifier :- s/Any] 25 | (p/delete-object! component qualifier)) 26 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/database.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. 2 | 3 | (ns bolt.dev.database 4 | (:require 5 | [com.stuartsierra.component :refer (Lifecycle)] 6 | [buddy.hashers :as hs] 7 | [schema.core :as s])) 8 | 9 | ;; A user database 10 | 11 | (defn seed-database [db] 12 | (reset! (:atom db) 13 | {:user "alice@example.org" :password (hs/encrypt "wonderland")}) 14 | db) 15 | 16 | (defrecord Database [] 17 | Lifecycle 18 | (start [component] 19 | (seed-database (assoc component :atom (atom {})))) 20 | (stop [component] component)) 21 | 22 | (def new-database-schema {}) 23 | 24 | (defn new-database [& {:as opts}] 25 | (->> opts 26 | (merge {}) 27 | (s/validate new-database-schema) 28 | map->Database)) 29 | -------------------------------------------------------------------------------- /dev/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | UTF-8 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | DEBUG 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/main.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. 2 | 3 | (ns bolt.dev.main 4 | "Main entry point" 5 | (:require clojure.pprint) 6 | (:gen-class)) 7 | 8 | (defn -main [& args] 9 | ;; We eval so that we don't AOT anything beyond this class 10 | (eval '(do 11 | (require 'bolt.dev.main) 12 | (require 'dev) 13 | (require 'com.stuartsierra.component) 14 | (require '[modular.component.co-dependency :as co-dependency]) 15 | 16 | (println "Starting bolt website") 17 | 18 | (let [system (-> 19 | (dev/new-dev-system) 20 | co-dependency/start-system)] 21 | 22 | (println "System started") 23 | (println "Ready...") 24 | 25 | )))) 26 | -------------------------------------------------------------------------------- /dev/resources/public/css/example-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | p { 6 | font-size: 16pt; 7 | } 8 | 9 | h1 { 10 | font-size: 24pt; 11 | } 12 | 13 | h2 { 14 | font-size: 20pt; 15 | } 16 | 17 | label { 18 | min-width: 6em; 19 | } 20 | 21 | form { 22 | font-size: 15pt; 23 | margin: 30px 0; 24 | } 25 | 26 | form div { 27 | margin: 10px 0; 28 | } 29 | 30 | form label { 31 | padding: 4px; 32 | } 33 | 34 | form input { 35 | width: 16.5em; 36 | margin: 0 1em; 37 | padding: 2px 4px; 38 | } 39 | 40 | form input#password { 41 | width: 8em; 42 | } 43 | 44 | form input.submit { 45 | width: 8em; 46 | /* margin-left is label min-width + input margin */ 47 | margin-left: 7em; 48 | } 49 | 50 | .footer { 51 | margin-top: 50px; 52 | } 53 | 54 | .footer p { 55 | font-size: 12pt; 56 | } 57 | -------------------------------------------------------------------------------- /src/bolt/session/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.session.protocols) 2 | 3 | (defprotocol SessionStore 4 | "A SessionStore maps an identifier, stored in a cookie, to a set of 5 | attributes. It is able to get cookies from the HTTP request, and set 6 | them on the HTTP response. A SessionStore will typically wrap a 7 | TokenStore." 8 | 9 | (session [_ req] 10 | "Returns the attribute map of the session, or nil if is no session") 11 | 12 | (assoc-session-data! [_ req m] 13 | "Associate data to an existing session. If there is no session, 14 | throw an exception.") 15 | 16 | (respond-with-new-session! [_ req data resp] 17 | "Create a new session with the given data, setting the cookie on the response") 18 | 19 | (respond-close-session! [_ req resp] 20 | "Delete the session from the store, response should inform the 21 | browser by setting the cookie with a 1970 expiry")) 22 | -------------------------------------------------------------------------------- /src/bolt/token_store/protocols.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.token-store.protocols) 4 | 5 | ;; All TokenStore implementations must provide temporary or persistent 6 | ;; storage and must expire tokens that are no longer valid. Expiry 7 | ;; policies are left to the implementor to decide. Token ids are 8 | ;; determined by the client, but are recommended to be resistent to 9 | ;; prediction and thus forgery (using HMAC, etc.). 10 | 11 | (defprotocol TokenStore 12 | (create-token! [_ id m] 13 | "Create a new token identified by id") 14 | (get-token-by-id [_ id] 15 | "Return the token identified by id") 16 | (purge-token! [_ id]) 17 | (renew-token! [_ id] 18 | "Renew the token so that it has a fresh expiry date. Returns the renewed token.") 19 | (merge-token! [_ id m] 20 | "Merge the token identified by id with the given map") 21 | (dissoc-token! [_ id ks])) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2014 JUXT LTD. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/bolt/session.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.session 4 | (:require 5 | [ring.middleware.cookies :refer (cookies-request cookies-response)] 6 | [bolt.session.protocols :as p] 7 | [bolt.util :refer (Request Response)] 8 | [schema.core :as s])) 9 | 10 | (s/defn session :- (s/maybe {s/Keyword s/Any}) 11 | [component :- (s/protocol p/SessionStore) 12 | request ;; :- Request 13 | ] 14 | (p/session component request)) 15 | 16 | (s/defn assoc-session-data! :- nil 17 | [component :- (s/protocol p/SessionStore) 18 | request ;; :- Request 19 | m :- {s/Keyword s/Any}] 20 | (p/assoc-session-data! component request m)) 21 | 22 | (s/defn respond-with-new-session! :- Response 23 | [component :- (s/protocol p/SessionStore) 24 | request ;; :- Request 25 | data :- {s/Keyword s/Any} 26 | response :- Response] 27 | (p/respond-with-new-session! component request data response)) 28 | 29 | (s/defn respond-close-session! :- Response 30 | [component :- (s/protocol p/SessionStore) 31 | request ;; :- Request 32 | response :- Response] 33 | (p/respond-close-session! component request response)) 34 | -------------------------------------------------------------------------------- /dev/resources/user-guide.md: -------------------------------------------------------------------------------- 1 | # The bolt user guide 2 | 3 | Welcome to the bolt user guide! 4 | 5 | ### bolt is simple, but also easy! 6 | 7 | If you follow this guide carefully you will learn how to take advantage 8 | of the many features bolt has to offer. 9 | 10 | If you are a more experienced Clojure or security expert developer and 11 | would like to get involved in influencing bolt's future, please join our 12 | [bolt-discuss](https://groups.google.com/forum/#!forum/bolt-discuss) 13 | discussion group. Regardless of your experience, everyone is more than 14 | welcome to join the list. List members will do their best to answer any 15 | questions you might have. 16 | 17 | ### Spot an error? 18 | 19 | If you spot a typo, misspelling, grammar problem, confusing text, or 20 | anything you feel you could improve, please go-ahead and 21 | [edit the source](https://github.com/juxt/bolt/edit/master/dev/resources/user-guide.md). If 22 | your contribution is accepted you will have our eternal gratitude, help 23 | future readers and be forever acknowledged in the yada documentation as 24 | a contributor! 25 | 26 | [Back to index](/) 27 | 28 | ### Table of Contents 29 | 30 | 31 | 32 | ## Introduction 33 | -------------------------------------------------------------------------------- /src/bolt/oauth/registry/ref_backed_registry.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.oauth.registry.ref-backed-registry 2 | (require 3 | [com.stuartsierra.component :as component] 4 | [bolt.oauth.registry.protocols :refer (ClientRegistry)])) 5 | 6 | ;; Optional ClientRegistry implementation 7 | 8 | (defrecord RefBackedClientRegistry [] 9 | component/Lifecycle 10 | (start [this] 11 | (assoc this :store {:last-client-id (ref 1000) 12 | :clients (ref {})})) 13 | (stop [this] this) 14 | 15 | ClientRegistry 16 | (register-client [this properties] 17 | (dosync 18 | (let [client-id (or (:client-id properties) 19 | (str (alter (-> this :store :last-client-id) inc))) 20 | properties (assoc properties 21 | :client-id client-id 22 | :client-secret (or (:client-secret properties) 23 | (str (java.util.UUID/randomUUID))))] 24 | (alter (-> this :store :clients) assoc client-id properties) 25 | (select-keys properties [:client-id :client-secret])))) 26 | 27 | (lookup-client [this client-id] 28 | (-> this :store :clients deref (get client-id)))) 29 | 30 | (defn new-ref-backed-client-registry [] 31 | (->RefBackedClientRegistry)) 32 | -------------------------------------------------------------------------------- /dev/resources/public/css/fonts.css: -------------------------------------------------------------------------------- 1 | /* latin-ext */ 2 | @font-face { 3 | font-family: 'Source Sans Pro'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(/static/fonts/latin-ext.woff2) format('woff2'); 7 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 8 | } 9 | /* latin */ 10 | @font-face { 11 | font-family: 'Source Sans Pro'; 12 | font-style: normal; 13 | font-weight: 400; 14 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(/static/fonts/latin.woff2) format('woff2'); 15 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 16 | } 17 | /* swedish sans */ 18 | @font-face { 19 | font-family: 'SwedenSans'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: url(/static/fonts/SwedenSans.otf); 23 | } 24 | @font-face { 25 | font-family: 'SwedenSans'; 26 | font-style: bold; 27 | font-weight: 700; 28 | src: url(/static/fonts/SwedenSansBold.otf); 29 | } 30 | @font-face { 31 | font-family: 'RochesterYada'; 32 | font-style: normal; 33 | font-weight: 400; 34 | src: local('RochesterYada'), local('RochesterYada-Regular'), url(/static/fonts/RochesterYada.woff2) format('woff2'); 35 | } -------------------------------------------------------------------------------- /dev/src/bolt/dev/website.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.dev.website 2 | (:require 3 | [schema.core :as s] 4 | [bidi.bidi :refer (RouteProvider tag)] 5 | [modular.bidi :refer (path-for)] 6 | [clojure.java.io :as io] 7 | [hiccup.core :refer (html)] 8 | [com.stuartsierra.component :refer (using)] 9 | [modular.template :as template :refer (render-template)] 10 | [modular.component.co-dependency :refer (co-using)] 11 | [yada.yada :refer (yada)])) 12 | 13 | (defn index [{:keys [*router templater]}] 14 | (yada 15 | :body 16 | {"text/html" 17 | (fn [ctx] 18 | (render-template 19 | templater 20 | "templates/page.html.mustache" 21 | {:content 22 | (html 23 | [:div.container 24 | [:h2 "Welcome to " [:span.bolt "bolt"] "!"] 25 | [:ol 26 | [:li [:a {:href (path-for @*router :bolt.dev.user-guide/user-guide)} 27 | "User guide"]]]])}))})) 28 | 29 | (defrecord Website [*router templater] 30 | RouteProvider 31 | (routes [component] 32 | ["/index.html" (-> (index component) 33 | (tag ::index))])) 34 | 35 | (defn new-website [& {:as opts}] 36 | (-> (->> opts 37 | (merge {}) 38 | (s/validate {}) 39 | map->Website) 40 | (using [:templater]) 41 | (co-using [:router]))) 42 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/view.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.dev.view 2 | (:require 3 | [clojure.tools.logging :refer :all] 4 | [modular.template :refer (render-template template-model)] 5 | [bolt.session :refer (session)])) 6 | 7 | (defn page-body 8 | "Render a page body, with the given templater and a (deferred) 9 | template-model spanning potentially numerous records satisfying 10 | modular.template's TemplateModel protocol." 11 | [templater template model] 12 | (render-template 13 | templater 14 | "templates/example1/page.html.mustache" 15 | (merge model 16 | {:title "Example 1" 17 | :content 18 | (render-template 19 | templater 20 | template 21 | model)}))) 22 | 23 | (defn page [template templater *template-model *router session-store] 24 | (fn [req] 25 | (infof "route is %s" @*router) 26 | (infof "template-model is %s" @*template-model) 27 | (infof "session-store is %s" (session session-store req)) 28 | (infof "templater is %s" templater) 29 | {:status 200 30 | :body (page-body templater template 31 | (merge 32 | (template-model @*template-model req) 33 | 34 | (when-let [user (some-> (session session-store req) :bolt/user)] 35 | {:user user})))})) 36 | -------------------------------------------------------------------------------- /src/bolt/token_store.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.token-store 4 | (:require 5 | [bolt.token-store.protocols :as p] 6 | [schema.core :as s])) 7 | 8 | (s/defschema Date "A Java date" 9 | (s/pred #(instance? java.util.Date %) "date")) 10 | 11 | (s/defschema Token "A token" 12 | {(s/optional-key :bolt/expiry) Date 13 | s/Keyword s/Any}) 14 | 15 | (s/defn create-token! :- Token 16 | [component :- (s/protocol p/TokenStore) 17 | id :- s/Str 18 | m :- {s/Keyword s/Any}] 19 | (p/create-token! component id m)) 20 | 21 | (s/defn get-token-by-id :- (s/maybe Token) 22 | [component :- (s/protocol p/TokenStore) 23 | id :- s/Str] 24 | (p/get-token-by-id component id)) 25 | 26 | (s/defn purge-token! :- nil 27 | [component :- (s/protocol p/TokenStore) 28 | id :- s/Str] 29 | (p/purge-token! component id)) 30 | 31 | (s/defn renew-token! :- (s/maybe Token) 32 | [component :- (s/protocol p/TokenStore) 33 | id :- s/Str] 34 | (p/renew-token! component id)) 35 | 36 | (s/defn merge-token! :- (s/maybe Token) 37 | [component :- (s/protocol p/TokenStore) 38 | id :- s/Str 39 | m :- {s/Keyword s/Any}] 40 | (p/merge-token! component id m)) 41 | 42 | (s/defn dissoc-token! :- (s/maybe Token) 43 | [component :- (s/protocol p/TokenStore) 44 | id :- s/Str 45 | ks :- #{s/Keyword}] 46 | (p/dissoc-token! component id ks)) 47 | -------------------------------------------------------------------------------- /dev/resources/public/js/examples.js: -------------------------------------------------------------------------------- 1 | clearIt = function(title) { 2 | var id = "response-"+title; 3 | 4 | // Clear 5 | $("div#"+id+" .status").text(""); 6 | $("div#"+id+" .headers").text(""); 7 | $("div#"+id+" .body").val(""); 8 | } 9 | 10 | tryIt = function(meth, u, title, headers) { 11 | clearIt(title); 12 | 13 | var id = "response-"+title; 14 | $("div#"+id+" .status").html("Waiting…"); 15 | 16 | $.ajax({type: meth, 17 | url: u, 18 | headers: headers}) 19 | .done(function(msg, textStatus, jqXHR) { 20 | $("div#"+id+" .status").text(jqXHR.status + " " + jqXHR.statusText); 21 | // $("div#"+id+" .headers").text(jqXHR.getAllResponseHeaders()); 22 | 23 | $("div#"+id+" .headers").html(jqXHR.getAllResponseHeaders()); 24 | $("div#"+id+" .body").val(jqXHR.responseText); 25 | }) 26 | .fail(function(jqXHR) { 27 | $("div#"+id+" .status").text(jqXHR.status); 28 | $("div#"+id+" .headers").text(jqXHR.getAllResponseHeaders()); 29 | $("div#"+id+" .body").val(jqXHR.responseText); 30 | }) 31 | } 32 | 33 | 34 | tryItEvents = function(meth, u, title, headers) { 35 | 36 | clearIt(title); 37 | 38 | var id = "response-"+title; 39 | $("div#"+id+" .status").html("See console…"); 40 | 41 | var es = new EventSource(u); 42 | es.onmessage = function(ev) { 43 | console.log(ev); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/bolt/oauth/registry.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.oauth.registry 4 | (:require 5 | [bolt.oauth.registry.protocols :as p] 6 | [schema.core :as s])) 7 | 8 | (s/defn register-client :- {:client-id s/Str 9 | :client-secret s/Str} 10 | [p :- (s/protocol p/ClientRegistry) 11 | ;; If client-id and/or client-secret are not specified, they will be 12 | ;; generated. 13 | properties :- {(s/optional-key :client-id) s/Str 14 | (s/optional-key :client-secret) s/Str 15 | :application-name s/Str 16 | :homepage-uri s/Str 17 | (s/optional-key :description) s/Str 18 | :redirection-uri s/Str 19 | :required-scopes (s/either #{s/Keyword} #{s/Str}) 20 | :requires-user-acceptance? s/Bool}] 21 | (p/register-client p properties)) 22 | 23 | (s/defn lookup-client :- {:application-name s/Str 24 | :homepage-uri s/Str 25 | (s/optional-key :description) s/Str 26 | :redirection-uri s/Str 27 | :client-id s/Str 28 | :client-secret s/Str 29 | :required-scopes (s/either #{s/Keyword} #{s/Str}) 30 | :requires-user-acceptance? s/Bool} 31 | [p :- (s/protocol p/ClientRegistry) 32 | client-id :- s/Str] 33 | (p/lookup-client p client-id)) 34 | -------------------------------------------------------------------------------- /dev/resources/public/js/tests.js: -------------------------------------------------------------------------------- 1 | clearIt = function(ix) { 2 | var id = "test-"+ix; 3 | 4 | // Clear 5 | $("#"+id+" .status").text(""); 6 | $("#"+id+" .headers").text(""); 7 | $("#"+id+" .body").val(""); 8 | } 9 | 10 | testResult = function(jqXHR, expectation) { 11 | if (jqXHR.status == expectation.status) { 12 | return "PASS"; 13 | } else { 14 | return "FAIL"; 15 | } 16 | } 17 | 18 | testIt = function(meth, u, ix, headers, expectation) { 19 | clearIt(ix); 20 | 21 | var id = "test-"+ix; 22 | $("#"+id+" .status").html("Waiting…"); 23 | 24 | $.ajax({type: meth, 25 | url: u, 26 | headers: headers}) 27 | .done(function(msg, textStatus, jqXHR) { 28 | $("#"+id+" .status").text(jqXHR.status + " " + jqXHR.statusText); 29 | $("#"+id+" .headers").html(jqXHR.getAllResponseHeaders()); 30 | $("#"+id+" .body").val(jqXHR.responseText); 31 | $("#"+id+" .result").html(testResult(jqXHR, expectation)); 32 | }) 33 | .fail(function(jqXHR) { 34 | $("#"+id+" .status").text(jqXHR.status); 35 | $("#"+id+" .headers").text(jqXHR.getAllResponseHeaders()); 36 | $("#"+id+" .body").val(jqXHR.responseText); 37 | $("#"+id+" .result").html(testResult(jqXHR, expectation)); 38 | }) 39 | } 40 | 41 | testAll = function() { 42 | $("button.test").click(); 43 | } 44 | 45 | testAll(); 46 | -------------------------------------------------------------------------------- /src/bolt/storage/atom_storage.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.storage.atom-storage 4 | (:require 5 | [com.stuartsierra.component :refer (Lifecycle using)] 6 | [bolt.storage.protocols :refer (Storage)] 7 | [clojure.java.io :as io] 8 | [clojure.pprint :refer (pprint)] 9 | [schema.core :as s] 10 | [schema.utils :refer [class-schema]])) 11 | 12 | (defn selector [qualifier] 13 | (let [ks (keys qualifier)] 14 | #(when (= (select-keys % ks) qualifier) %))) 15 | 16 | (defn remove-object [ds qualifier seed] 17 | (into seed (remove (selector qualifier) ds))) 18 | 19 | (s/defschema Seed 20 | (s/either (s/eq #{}) (s/eq []))) 21 | 22 | (s/defrecord AtomStorage 23 | [ref :- (s/pred (partial instance? clojure.lang.Ref)) 24 | seed :- Seed] 25 | 26 | Lifecycle 27 | (start [component] (s/validate (class-schema (type component)) component)) 28 | (stop [component] component) 29 | 30 | Storage 31 | (find-object [component qualifier] 32 | (let [ks (keys qualifier)] 33 | (some (selector qualifier) @ref))) 34 | 35 | (store-object! [component object] 36 | (dosync 37 | (alter ref conj object))) 38 | 39 | (delete-object! [component qualifier] 40 | (dosync 41 | (alter ref remove-object qualifier seed)))) 42 | 43 | (defn new-atom-storage [& {:as opts}] 44 | (let [seed #{}] 45 | (->> opts 46 | (merge {:seed seed :ref (ref seed)}) 47 | map->AtomStorage))) 48 | -------------------------------------------------------------------------------- /src/bolt/authorization.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.authorization 4 | (:require 5 | [bolt.authentication :refer (get-subject-identifier)] 6 | [clojure.set :as set])) 7 | 8 | (defn behalf-of? [authenticator req user] 9 | (= (get-subject-identifier authenticator req) user)) 10 | 11 | ;; Role based logic, possibly deprecated but could be adapted. 12 | 13 | (defprotocol RoleQualifier 14 | (matches-role? [_ role])) 15 | 16 | (extend-protocol RoleQualifier 17 | clojure.lang.Keyword 18 | (matches-role? [this roles] 19 | (roles this)) 20 | 21 | clojure.lang.PersistentHashSet 22 | (matches-role? [this roles] 23 | (let [res (set/intersection this roles)] 24 | (when (not-empty res) res))) 25 | 26 | clojure.lang.PersistentVector 27 | (matches-role? [this roles] 28 | (when (every? #(matches-role? % roles) this) 29 | this)) 30 | 31 | Boolean 32 | (matches-role? [this roles] this)) 33 | 34 | (defprotocol UserRoles 35 | (user-in-role? [_ user role])) 36 | 37 | (extend-protocol UserRoles 38 | clojure.lang.PersistentArrayMap 39 | (user-in-role? [this user role] 40 | (when-let [roles (get this user)] 41 | (matches-role? role roles))) 42 | 43 | clojure.lang.PersistentHashMap 44 | (user-in-role? [this user role] 45 | (when-let [roles (get this user)] 46 | (matches-role? role roles))) 47 | 48 | clojure.lang.Fn 49 | (user-in-role? [this user role] 50 | (this user role)) 51 | 52 | nil 53 | (user-in-role? [this user role] 54 | nil)) 55 | -------------------------------------------------------------------------------- /src/bolt/user/email_user_store.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user.email-user-store 4 | (:require 5 | [clojure.tools.logging :refer :all] 6 | [bolt.user :refer (check-create-user)] 7 | [bolt.user.protocols :refer (UserStore)] 8 | [bolt.storage.protocols :refer (find-object store-object!)] 9 | [com.stuartsierra.component :refer (Lifecycle using)] 10 | [clojure.pprint :refer (pprint)] 11 | [clojure.java.io :as io] 12 | [schema.core :as s] 13 | [plumbing.core :refer (<-)] 14 | )) 15 | 16 | ;; This user store only looks for :email as the only identifier and 17 | ;; uniquely differentiating factor of a user. 18 | (defrecord EmailUserStore [storage] 19 | 20 | UserStore 21 | (check-create-user [component user] 22 | (let [user (select-keys user [:user]) 23 | existing (find-object storage user)] 24 | (when existing 25 | {:error :user-exists 26 | :user user}))) 27 | 28 | (create-user! [component user] 29 | (or 30 | (check-create-user component user) 31 | (do 32 | (store-object! storage user) 33 | user))) 34 | 35 | (find-user [component id] 36 | (find-object storage {:email id})) 37 | 38 | (update-user! [component id user] 39 | (throw (ex-info "TODO" {}))) 40 | 41 | (delete-user! [_ id] 42 | (throw (ex-info "TODO" {}))) 43 | 44 | (verify-email! [_ email] 45 | (throw (ex-info "TODO" {})))) 46 | 47 | (defn new-email-user-store [& {:as opts}] 48 | (->> opts 49 | map->EmailUserStore 50 | (<- (using [:storage])))) 51 | -------------------------------------------------------------------------------- /test/bolt/roles_test.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.roles-test 2 | (:require 3 | [bolt.authorization :refer :all] 4 | [clojure.test :refer :all] 5 | [clojure.set :as set])) 6 | 7 | (deftest user-roles 8 | (testing "map" 9 | (let [roles {"alice" #{:accountant :clerk} 10 | "bob" #{:clerk}}] 11 | (are [user roleq _ result] (is (= (user-in-role? roles user roleq) result)) 12 | "alice" :accountant => :accountant 13 | "alice" :clerk => :clerk 14 | "alice" #{:accountant :superuser} => #{:accountant} 15 | "bob" #{:accountant :superuser} => nil 16 | "bob" :accountant => nil 17 | "bob" #{:accountant :clerk} => #{:clerk} 18 | "alice" [:accountant :clerk] => [:accountant :clerk] 19 | "bob" [:accountant :clerk] => nil 20 | ;; Booleans are useful for testing 21 | "alice" true => true 22 | "bob" false => false 23 | ))) 24 | (testing "fn" 25 | (let [roles (fn [user role] (= user "alice"))] 26 | (are [user roleq _ result] (is (= (user-in-role? roles user roleq) result)) 27 | "alice" :accountant => true 28 | "alice" :clerk => true 29 | "bob" :clerk => false 30 | ))) 31 | ;; Nil needs to be supported in cases where no user roles have been established. 32 | (testing "nil" 33 | (let [roles nil] 34 | (are [user roleq _ result] (is (= (user-in-role? roles user roleq) result)) 35 | "alice" :accountant => nil 36 | "alice" :clerk => nil 37 | "bob" :clerk => nil 38 | )))) 39 | -------------------------------------------------------------------------------- /src/bolt/oauth/server/logout.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.oauth.server.logout 2 | (:require 3 | [bidi.bidi :refer (RouteProvider tag)] 4 | [com.stuartsierra.component :refer (using)] 5 | [bolt.token-store :refer (purge-token!)] 6 | [bolt.session :refer (session respond-close-session!)] 7 | [ring.util.response :refer (redirect)] 8 | [ring.middleware.params :refer (params-request)] 9 | [schema.core :as s] 10 | [plumbing.core :refer (<-)])) 11 | 12 | (defrecord Logout [session-store uri-context] 13 | RouteProvider 14 | (routes [component] 15 | [uri-context 16 | {"logout" 17 | (-> 18 | (fn [req] 19 | ;; Logout 20 | ;; TODO :- 21 | ;; "At the logout endpoint, the OP SHOULD ask the 22 | ;; End-User whether he wants to log out of the OP as 23 | ;; well. If the End-User says "yes", then the OP MUST 24 | ;; log out the End-User." -- 25 | ;; http://openid.net/specs/openid-connect-session-1_0.html 26 | 27 | ;; When there is an access-token associated with this session, we 28 | ;; shall purge it. 29 | (when-let [access-token (:bolt/access-token (session session-store req))] 30 | (purge-token! session-store access-token)) 31 | 32 | (let [post-logout-redirect-uri 33 | (-> req params-request :query-params (get "post_logout_redirect_uri"))] 34 | (respond-close-session! 35 | session-store req 36 | (if post-logout-redirect-uri 37 | (redirect post-logout-redirect-uri) 38 | {:status 200 :body "Logged out of auth server"})))) 39 | (tag ::logout))}])) 40 | 41 | (def new-logout-schema 42 | {:uri-context s/Str}) 43 | 44 | (defn new-logout [& {:as opts}] 45 | (->> opts 46 | (merge {:uri-context ""}) 47 | (s/validate new-logout-schema) 48 | map->Logout 49 | (<- (using [:session-store])))) 50 | -------------------------------------------------------------------------------- /dev/src/dev.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. 2 | 3 | (ns dev 4 | (:require 5 | [clojure.pprint :refer (pprint)] 6 | [clojure.reflect :refer (reflect)] 7 | [clojure.repl :refer (apropos dir doc find-doc pst source)] 8 | [clojure.tools.namespace.repl :refer (refresh refresh-all)] 9 | [clojure.java.io :as io] 10 | [com.stuartsierra.component :as component] 11 | [modular.component.co-dependency :as co-dependency] 12 | [bolt.dev.system :refer (config new-system-map new-dependency-map new-co-dependency-map)] 13 | [modular.maker :refer (make)] 14 | [bidi.bidi :as bidi])) 15 | 16 | (def system nil) 17 | 18 | (defn new-dev-system 19 | "Create a development system" 20 | [] 21 | (let [config (config) 22 | s-map (-> 23 | (new-system-map config) 24 | #_(assoc 25 | ))] 26 | (-> s-map 27 | (component/system-using (new-dependency-map)) 28 | (co-dependency/system-co-using (new-co-dependency-map)) 29 | ))) 30 | 31 | (defn init 32 | "Constructs the current development system." 33 | [] 34 | (alter-var-root #'system 35 | (constantly (new-dev-system)))) 36 | 37 | (defn start 38 | "Starts the current development system." 39 | [] 40 | (alter-var-root 41 | #'system 42 | co-dependency/start-system 43 | )) 44 | 45 | (defn stop 46 | "Shuts down and destroys the current development system." 47 | [] 48 | (alter-var-root #'system 49 | (fn [s] (when s (component/stop s))))) 50 | 51 | (defn go 52 | "Initializes the current development system and starts it running." 53 | [] 54 | (init) 55 | (start) 56 | :ok 57 | ) 58 | 59 | (defn reset [] 60 | (stop) 61 | (refresh :after 'dev/go)) 62 | 63 | 64 | 65 | (defn routes [] 66 | (-> system :router :routes)) 67 | 68 | (defn match-route [path & args] 69 | (apply bidi/match-route (routes) path args)) 70 | 71 | (defn path-for [target & args] 72 | (apply bidi/path-for (routes) target args)) 73 | -------------------------------------------------------------------------------- /src/bolt/user/totp.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user.totp) 4 | 5 | (defprotocol OneTimePasswordStore 6 | (set-totp-secret [_ identity secret] "this level add security to previous checked user/password identity") 7 | (get-totp-secret [_ identity] "Returns nil if no TOTP secret")) 8 | 9 | 10 | ;; Google Authenticator clojure code 11 | ;; taken from http://nakkaya.com/2012/08/13/google-hotp-totp-two-factor-authentication-for-clojure/ 12 | 13 | (defn secret-key [] 14 | (let [buff (make-array Byte/TYPE 10)] 15 | (-> (java.security.SecureRandom.) 16 | (.nextBytes buff)) 17 | 18 | (-> (org.apache.commons.codec.binary.Base32.) 19 | (.encode buff) 20 | (String.)))) 21 | 22 | (defn qr-code [identifier secret] 23 | (format (str "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr" 24 | "&chl=otpauth://totp/%s%%3Fsecret%%3D%s") 25 | identifier secret)) 26 | 27 | (defn hotp-token [secret idx] 28 | (let [secret (-> (org.apache.commons.codec.binary.Base32.) 29 | (.decode secret)) 30 | idx (-> (java.nio.ByteBuffer/allocate 8) 31 | (.putLong idx) 32 | (.array)) 33 | key-spec (javax.crypto.spec.SecretKeySpec. secret "HmacSHA1") 34 | mac (doto (javax.crypto.Mac/getInstance "HmacSHA1") 35 | (.init key-spec)) 36 | hash (->> (.doFinal mac idx) 37 | (into []))] 38 | 39 | (let [offset (bit-and (hash 19) 0xf) 40 | bin-code (bit-or (bit-shift-left (bit-and (hash offset) 0x7f) 24) 41 | (bit-shift-left (bit-and (hash (+ offset 1)) 0xff) 16) 42 | (bit-shift-left (bit-and (hash (+ offset 2)) 0xff) 8) 43 | (bit-and (hash (+ offset 3)) 0xff))] 44 | (format "%06d" (mod bin-code 1000000))))) 45 | 46 | (defn totp-token [secret] 47 | (hotp-token secret (/ (System/currentTimeMillis) 1000 30))) 48 | -------------------------------------------------------------------------------- /dev/resources/templates/example1/page.html.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{title}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 49 | 50 | 51 |
52 | {{{content}}} 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/bolt/util.clj: -------------------------------------------------------------------------------- 1 | ;; For internal Bolt use only. Not part of a published API. Do not use. 2 | (ns bolt.util 3 | (:require 4 | [schema.core :as s] 5 | [clojure.string :as str] 6 | #_[camel-snake-kebab :as csk]) 7 | (:import (java.net URLEncoder))) 8 | 9 | (defprotocol KorksSet 10 | (as-set [_])) 11 | 12 | (extend-protocol KorksSet 13 | clojure.lang.Keyword 14 | (as-set [k] #{k}) 15 | clojure.lang.PersistentHashSet 16 | (as-set [ks] ks) 17 | clojure.lang.PersistentVector 18 | (as-set [v] (set v)) 19 | clojure.lang.PersistentList 20 | (as-set [l] (set l))) 21 | 22 | (defn uri-with-qs [req] 23 | (str (:uri req) 24 | (when-let [qs (:query-string req)] (when (not-empty qs) (str "?" qs ))))) 25 | 26 | (defn absolute-prefix [req] 27 | (apply format "%s://%s:%s" 28 | ((juxt (comp name :scheme) :server-name :server-port) 29 | req))) 30 | 31 | (defn absolute-uri [req] 32 | (str (absolute-prefix req) (uri-with-qs req))) 33 | 34 | (defn as-www-form-urlencoded [m] 35 | (->> 36 | (map (fn [[k v]] (format "%s=%s" k (URLEncoder/encode v))) m) 37 | (interpose "&") 38 | (apply str))) 39 | 40 | (defn as-query-string [m] 41 | (->> 42 | (map (comp (partial apply str) 43 | (partial interpose "=")) 44 | m) 45 | (interpose "&") 46 | (cons "?") 47 | (apply str))) 48 | 49 | ;; Schema 50 | 51 | (s/defschema Request "A Ring-style request" 52 | {:headers s/Any 53 | s/Keyword s/Any}) 54 | 55 | (s/defschema Response "A Ring-style response" 56 | {(s/optional-key :status) s/Num 57 | (s/optional-key :headers) s/Any 58 | (s/optional-key :body) s/Str}) 59 | 60 | ;; Schema validation 61 | 62 | (defn wrap-schema-validation [h] 63 | (fn [req] 64 | (s/with-fn-validation 65 | (h req)))) 66 | 67 | 68 | ;; MD5 for gravatars 69 | 70 | (defn md5 [s] 71 | (let [algorithm (java.security.MessageDigest/getInstance "MD5") 72 | size (* 2 (.getDigestLength algorithm)) 73 | raw (.digest algorithm (.getBytes s)) 74 | sig (.toString (java.math.BigInteger. 1 raw) 16) 75 | padding (apply str (repeat (- size (count sig)) "0"))] 76 | (str padding sig))) 77 | 78 | ;; Misc 79 | 80 | (defn keywordize-form [m] 81 | (into {} (for [[k v] m] [(keyword (str/replace k "_" "-")) v]))) 82 | -------------------------------------------------------------------------------- /test/bolt/atom_backed_store_test.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.atom-backed-store-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [bolt.token-store.atom-backed-store :refer :all] 5 | [bolt.token-store :refer :all] 6 | [schema.core :as s])) 7 | 8 | (defn fn-validation-fixture [f] 9 | (s/with-fn-validation 10 | (f))) 11 | 12 | (use-fixtures :each fn-validation-fixture) 13 | 14 | ;; We must only run these tests when the code it is testing 15 | ;; changes. It's OK to use time based tests to be really sure of the 16 | ;; behaviour. 17 | 18 | (deftest non-expiry-tests 19 | (testing "create token" 20 | (let [tokens (atom {}) 21 | component (->AtomBackedTokenStore nil tokens)] 22 | (create-token! component "123" {:a "A"}) 23 | (is (= {:a "A" :bolt/token-id "123"} (get-token-by-id component "123"))))) 24 | 25 | (testing "renew token should return the new token" 26 | (let [tokens (atom {}) 27 | component (->AtomBackedTokenStore nil tokens)] 28 | (create-token! component "123" {:a "A"}) 29 | (is (= {:a "A" :bolt/token-id "123"} (get-token-by-id component "123"))) 30 | (let [token (renew-token! component "123")] 31 | (is (= {:a "A" :bolt/token-id "123"} token)))))) 32 | 33 | (deftest expiry-tests 34 | (testing "create token" 35 | (let [ttl 1 36 | tokens (atom {}) 37 | component (->AtomBackedTokenStore 1 tokens)] 38 | (create-token! component "123" {:a "A"}) 39 | (is (= {:a "A"} (select-keys (get-token-by-id component "123") [:a]))) 40 | (is (contains? (get-token-by-id component "123") :bolt/expiry)))) 41 | 42 | (testing "expiry" 43 | (let [ttl 1 44 | tokens (atom {}) 45 | component (->AtomBackedTokenStore 1 tokens)] 46 | (create-token! component "123" {:a "A"}) 47 | (is (not (nil? (get-token-by-id component "123")))) 48 | (Thread/sleep 1000) 49 | (is (nil? (get-token-by-id component "123"))))) 50 | 51 | (testing "renewal on get" 52 | (let [ttl 1 53 | tokens (atom {}) 54 | component (->AtomBackedTokenStore 1 tokens)] 55 | (create-token! component "123" {:a "A"}) 56 | (is (not (nil? (get-token-by-id component "123")))) 57 | (Thread/sleep 500) 58 | (is (not (nil? (get-token-by-id component "123")))) 59 | (Thread/sleep 750) 60 | (is (not (nil? (get-token-by-id component "123")))) 61 | (Thread/sleep 1000) 62 | (is (nil? (get-token-by-id component "123")))))) 63 | -------------------------------------------------------------------------------- /src/bolt/token_store/atom_backed_store.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.token-store.atom-backed-store 2 | (:require 3 | [bolt.token-store.protocols :refer (TokenStore get-token-by-id renew-token! purge-token!)] 4 | [com.stuartsierra.component :refer (Lifecycle)] 5 | [schema.core :as s] 6 | )) 7 | 8 | (defn expiry-date 9 | "Calculate an expiry date in the future" 10 | [ttl-in-secs] 11 | (assert (pos? ttl-in-secs)) 12 | (java.util.Date. 13 | (+ (.getTime (java.util.Date.)) 14 | (* ttl-in-secs 1000)))) 15 | 16 | (defn now 17 | "Return now in milliseconds since the epoch" 18 | [] 19 | (.getTime (java.util.Date.))) 20 | 21 | (defrecord AtomBackedTokenStore [ttl-in-secs tokens] 22 | TokenStore 23 | (create-token! [component id m] 24 | (when (get-token-by-id component id) 25 | (throw (ex-info "Token id already used" {:id id}))) 26 | (let [token (merge (when ttl-in-secs {:bolt/expiry (expiry-date ttl-in-secs)}) (merge {:bolt/token-id id} m))] 27 | (swap! tokens assoc id token) 28 | token)) 29 | 30 | (get-token-by-id [component id] 31 | (let [token (get @tokens id) 32 | expiry (:bolt/expiry token)] 33 | (cond 34 | (nil? expiry) token 35 | (< (now) (.getTime expiry)) (renew-token! component id) 36 | :otherwise (purge-token! component id)))) 37 | 38 | (purge-token! [_ id] 39 | (swap! tokens dissoc id) 40 | nil) 41 | 42 | (renew-token! [_ id] 43 | (swap! tokens update-in [id] 44 | #(if (:bolt/expiry %) 45 | (assoc % :bolt/expiry (expiry-date ttl-in-secs)) 46 | %)) 47 | ;; Return the renewed token 48 | (get @tokens id)) 49 | 50 | (merge-token! [component id m] 51 | (if-let [token (get-token-by-id component id)] 52 | (let [newtoken (merge token m)] 53 | (swap! tokens assoc id newtoken) 54 | newtoken))) 55 | 56 | (dissoc-token! [component id ks] 57 | (if-let [token (get-token-by-id component id)] 58 | (let [newtoken (dissoc token ks)] 59 | (swap! tokens assoc id newtoken) 60 | newtoken)))) 61 | 62 | (def new-atom-backed-token-store-schema 63 | {:ttl-in-secs (s/maybe s/Num) ; nil means 'do not expire' 64 | :tokens s/Any}) 65 | 66 | (defn new-atom-backed-token-store [& {:as opts}] 67 | (->> opts 68 | ;; TODO: Would prefer to rename ttl-in-secs to ttl and require a 69 | ;; Joda time period. 70 | (merge {:ttl-in-secs (* 60 60 4) 71 | :tokens (atom {})}) 72 | (s/validate new-atom-backed-token-store-schema) 73 | map->AtomBackedTokenStore)) 74 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/login_form.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.dev.login-form 2 | (:require 3 | [bolt.user.protocols :refer (LoginFormRenderer)] 4 | [bolt.dev.view :refer (page-body)] 5 | [modular.template :refer (template-model)] 6 | [hiccup.core :refer (html)] 7 | [modular.bidi :refer (path-for)] 8 | [com.stuartsierra.component :refer (using)] 9 | [modular.component.co-dependency :refer (co-using)])) 10 | 11 | (defrecord LoginForm [templater *template-model *router] 12 | LoginFormRenderer 13 | (render-login-form [component req model] 14 | (page-body 15 | templater "templates/example1/dialog.html.mustache" 16 | (merge (template-model @*template-model req) 17 | {:title (:title model) 18 | :form 19 | (html 20 | (when (:login-failed? model) 21 | [:div.alert.alert-danger.alert-dismissible 22 | {:role "alert"} 23 | [:button.close {:type "button" :data-dismiss "alert" :aria-label "Close"} 24 | [:span {:aria-hidden "true"} "×"] 25 | ] 26 | "You have entered an unrecognised email address or incorrect password."] 27 | ) 28 | [:form {:action (-> model :form :action) 29 | :method (-> model :form :method)} 30 | (when-let [redirect (:post-login-redirect model)] 31 | [:input {:type :hidden :name "post_login_redirect" :value redirect}] 32 | ) 33 | [:div 34 | [:label {:for "email"} "Email"] 35 | ;; We must have 36 | [:input#email {:type :text :name "user"}]] 37 | [:div 38 | [:label {:for "password"} "Password"] 39 | [:input#password {:type :password :name "password"}] 40 | (when-let [href nil] 41 | [:a {:href href} "Forgot password"])] 42 | [:div 43 | [:input.submit {:type "submit" :value "Sign in"}] 44 | ;; If we can't find a path to the signup form, we deduce 45 | ;; that no signup functionality exists. This is the 46 | ;; feature toggle. 47 | (when-let [signup (path-for @*router :bolt.user.signup/GET-signup-form)] 48 | [:a {:href signup } "Sign up"])] 49 | 50 | ])}))) 51 | ) 52 | 53 | (defn new-login-form [& {:as args}] 54 | (-> 55 | (->> args 56 | (merge {}) 57 | map->LoginForm) 58 | (using [:templater]) 59 | (co-using [:template-model :router]))) 60 | -------------------------------------------------------------------------------- /src/bolt/storage/file_storage.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. All Rights Reserved. 2 | 3 | (ns ^{:doc "This implementation is merely for example purposes to help 4 | with getting started examples and remove the need to set up and 5 | configure databases. For serious purposes, replace this implementation 6 | with one using a proper data store."} 7 | bolt.storage.file-storage 8 | (:require 9 | [com.stuartsierra.component :refer (Lifecycle using)] 10 | [bolt.storage.protocols :refer (Storage find-object store-object! delete-object!)] 11 | [bolt.storage.atom-storage :refer (new-atom-storage)] 12 | [clojure.java.io :as io] 13 | [clojure.pprint :refer (pprint)] 14 | [schema.core :as s])) 15 | 16 | (defn- save-file 17 | "Save the state of the component's ref to a file, via an agent." 18 | [{:keys [agent atom-storage file]}] 19 | (send-off 20 | agent 21 | (fn [f] 22 | (spit f (with-out-str (pprint @(:ref atom-storage)))) 23 | file))) 24 | 25 | (defn selector [qualifier] 26 | (let [ks (keys qualifier)] 27 | #(when (= (select-keys % ks) qualifier) %))) 28 | 29 | (defn remove-object [ds qualifier seed] 30 | (into seed (remove (selector qualifier) ds))) 31 | 32 | (defrecord FileStorage [file seed atom-storage] 33 | Storage 34 | (find-object [component qualifier] 35 | (find-object atom-storage qualifier)) 36 | 37 | (store-object! [component object] 38 | (dosync 39 | (store-object! atom-storage object) 40 | (save-file component))) 41 | 42 | (delete-object! [component qualifier] 43 | (dosync 44 | (delete-object! atom-storage qualifier) 45 | (save-file component)))) 46 | 47 | (defn- check-file-parent [{f :file :as opts}] 48 | (assert 49 | (.exists (.getParentFile (.getCanonicalFile f))) 50 | (format "Please create the directory structure which should contain the file: %s" f)) 51 | opts) 52 | 53 | (defn add-ref-agent [{f :file seed :seed :as m}] 54 | (assoc m 55 | :seed seed 56 | :ref (ref 57 | (if (.exists f) 58 | (read-string (slurp f)) 59 | seed)) 60 | :agent (agent f))) 61 | 62 | (defn new-file-storage [& {:as opts}] 63 | (->> opts 64 | (merge {:seed #{} 65 | :atom-storage (new-atom-storage (or (:seed opts) #{}))}) 66 | (s/validate {:file (s/either s/Str (s/pred (partial instance? java.io.File))) 67 | :seed (s/either (s/eq #{}) (s/eq [])) 68 | :atom-storage (s/protocol Storage)}) 69 | (#(update-in % [:file] io/file)) 70 | check-file-parent 71 | add-ref-agent 72 | map->FileStorage)) 73 | -------------------------------------------------------------------------------- /src/bolt/oauth/resource.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.oauth.resource 4 | (:require 5 | [clojure.string :as str] 6 | [com.stuartsierra.component :refer (Lifecycle using)] 7 | [bolt.token-store :refer (get-token-by-id)] 8 | [bolt.token-store.protocols :refer (TokenStore)] 9 | [bolt.authentication.protocols :refer (RequestAuthenticator)] 10 | [bolt.authentication :refer (authenticate)] 11 | [schema.core :as s] 12 | [plumbing.core :refer (<-)])) 13 | 14 | ;; Having established an access token store, we can now use it to verify 15 | ;; incoming requests bearing access tokens. 16 | 17 | (defrecord AccessTokenRequestAuthenticator [access-token-store] 18 | Lifecycle 19 | (start [component] 20 | (s/validate 21 | {:access-token-store (s/protocol TokenStore)} 22 | component)) 23 | (stop [component] component) 24 | 25 | RequestAuthenticator 26 | (authenticate [component request] 27 | (when-let [auth-header (get (:headers request) "authorization")] 28 | ;; Only match 'Bearer' tokens for now 29 | (when-let [access-token (second (re-matches #"\QBearer\E\s+(.*)" auth-header))] 30 | (get-token-by-id access-token-store access-token))))) 31 | 32 | (defn new-access-token-request-authenticator [& {:as opts}] 33 | (->> opts 34 | (merge {}) 35 | map->AccessTokenRequestAuthenticator 36 | (<- (using [:access-token-store])))) 37 | 38 | ;; Personal access tokens can be created. Same as GitHub's 'personal 39 | ;; access tokens'. 40 | 41 | (def new-personal-access-token-request-authenticator-schema 42 | {:header-token s/Str}) 43 | 44 | (defrecord PersonalAccessTokenRequestAuthenticator [header-token token-store] 45 | Lifecycle 46 | (start [component] 47 | (s/validate 48 | (merge new-personal-access-token-request-authenticator-schema 49 | {:token-store (s/protocol TokenStore)}) 50 | component) 51 | (assoc component 52 | :pattern (re-pattern (format "\\Q%s\\E\\s+(.*)" header-token)))) 53 | (stop [component] component) 54 | 55 | RequestAuthenticator 56 | (authenticate [component request] 57 | (when-let [auth-header (get (:headers request) "authorization")] 58 | (when-let [token-id (second (re-matches (:pattern component) (str/trim auth-header)))] 59 | (get-token-by-id token-store token-id))))) 60 | 61 | (defn new-personal-access-token-request-authenticator [& {:as opts}] 62 | (->> opts 63 | (merge {}) 64 | (s/validate new-personal-access-token-request-authenticator-schema) 65 | map->PersonalAccessTokenRequestAuthenticator 66 | (<- (using [:token-store])))) 67 | -------------------------------------------------------------------------------- /src/bolt/oauth/client.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.oauth.client 2 | (:require 3 | [clojure.tools.logging :refer :all] 4 | [bolt.authentication :refer (authenticate)] 5 | [bolt.util :refer (absolute-uri)] 6 | [schema.core :as s])) 7 | 8 | ;; I don't think this is a wonderful name but until we can think of 9 | ;; something better :) 10 | (defprotocol AccessTokenGrantee 11 | (solicit-access-token 12 | [_ req uri] 13 | [_ req uri scope-korks] 14 | "Initiate a process (typically via a HTTP redirect) that will result 15 | in a new request being made with an access token, if possible. Don't 16 | request specific scopes but get the defaults for the client.") 17 | 18 | (expired? [_ req access-token]) 19 | 20 | (refresh-access-token [_ req] 21 | "Initiate a process (typically via a HTTP redirect) that will result 22 | in a new request being made with an access token, if possible." 23 | )) 24 | 25 | ;; Ring middleware to restrict a handler to a given role. 26 | ;; The algo in here should fit many usages. However, other functions 27 | ;; could be provided to implement different policies. 28 | 29 | (defn wrap-require-authorization 30 | "Restrict a handler to a role. :identity and :access-token are added 31 | to the request. If a role is specified, also check that the role 32 | exists in the scope of the client. If role isn't specified, the 33 | identity and access-token are still retrieved." 34 | [h client & [scope]] 35 | (fn [req] 36 | (let [{access-token :bolt/access-token 37 | scopes :bolt/scopes 38 | sub :bolt/subject-identifier 39 | :as user} 40 | (authenticate client req)] 41 | 42 | (cond 43 | (nil? access-token) 44 | (do 45 | (debugf "No access token, so soliciting one from client %s" client) 46 | (solicit-access-token client req (absolute-uri req))) 47 | (expired? client req access-token) 48 | (do 49 | (debugf "access token has expired, seeking to refresh it") 50 | ;; The thinking here is that any refresh token that was returned 51 | ;; to the client will still be held by the client and can be 52 | ;; used to refresh the access-token 53 | (refresh-access-token client req)) 54 | 55 | (and scope (not (contains? scopes scope))) 56 | ;; TODO Must do something better than this 57 | {:status 401 :body "Sorry, you just don't have enough privileges to access this page"} 58 | 59 | :otherwise 60 | (h (assoc req 61 | :bolt/user user 62 | ;; Deprecated 63 | :bolt/subject-identifier sub 64 | :bolt/access-token access-token)))))) 65 | -------------------------------------------------------------------------------- /dev/resources/templates/page.html.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | bolt 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |

bolt is a work in progress, consider alpha status. Details are subject to change.

39 |
40 | 41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | {{{sidebar}}} 50 |
51 |
52 | {{{content}}} 53 |
54 |
55 | 56 |
57 |
58 | {{{sidebar}}} 59 |
60 |
61 |
62 |
63 |
64 |

bolt
Copyright © 2015, LTD.

65 |
66 |
67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {{#scripts}} 82 | 83 | {{/scripts}} 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/bolt/authentication.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.authentication 4 | (:require 5 | [clojure.string :as str] 6 | [clojure.tools.logging :refer :all] 7 | [bolt.authentication.protocols :as p] 8 | [bolt.util :refer (Request Response)] 9 | [bolt.session :refer (session)] 10 | [bolt.session.protocols :refer (SessionStore)] 11 | [schema.core :as s] 12 | [com.stuartsierra.component :refer (using)] 13 | [plumbing.core :refer (<-)])) 14 | 15 | ;; RequestAuthenticator 16 | 17 | (s/defn authenticate :- (s/maybe {s/Keyword s/Any}) 18 | [component :- (s/protocol p/RequestAuthenticator) 19 | request :- Request] 20 | (p/authenticate component request)) 21 | 22 | ;; An authenticator that checks an authorization header. It takes a map, 23 | ;; which maps Authorization token types to the local keywords of its 24 | ;; dependencies that process them. Using declarations are automatically 25 | ;; added. 26 | 27 | (defrecord AuthorizationHeaderRequestAuthenticator [mappings] 28 | p/RequestAuthenticator 29 | (authenticate [this request] 30 | (when-let [header (get-in request [:headers "authorization"])] 31 | (let [token-type (first (str/split (str/trim header) #"\s")) 32 | dependency (get mappings token-type)] 33 | (if-let [delegate-authenticator (get this dependency)] 34 | (authenticate delegate-authenticator request) 35 | (debugf "Unrecognized token type (%s -> %s) in incoming Authorization header, with mappings as %s" token-type dependency mappings)))))) 36 | 37 | (def new-authorization-header-request-authenticator-schema 38 | {:mappings {s/Str s/Keyword}}) 39 | 40 | (defn new-authorization-header-request-authenticator [& {:as opts}] 41 | (->> opts 42 | (merge {}) 43 | (s/validate new-authorization-header-request-authenticator-schema) 44 | (map->AuthorizationHeaderRequestAuthenticator) 45 | (<- (using (-> opts :mappings vals vec))))) 46 | 47 | ;; 48 | 49 | ;; Either - try multiple authenticators until one returns 50 | 51 | (defrecord EitherRequestAuthenticator [] 52 | p/RequestAuthenticator 53 | (authenticate [this request] 54 | (debugf "Either: %s" (keys this)) 55 | (some (fn [a] 56 | (when (satisfies? p/RequestAuthenticator a) 57 | (let [res (authenticate a request)] 58 | (when res (assoc res ::authenticator a))))) 59 | (vals this)))) 60 | 61 | (defn new-either-request-authenticator [& {:as opts}] 62 | (->> opts 63 | (merge {}) 64 | (map->EitherRequestAuthenticator))) 65 | 66 | ;; Utility functions 67 | 68 | ;; TODO: It's possible that it would be useful to memoize authentication 69 | ;; on each request 70 | 71 | (defn get-subject-identifier [authenticator req] 72 | (:bolt/subject-identifier (authenticate authenticator req))) 73 | 74 | (s/defn initiate-authentication-handshake :- Response 75 | [component :- (s/protocol p/AuthenticationHandshake) 76 | request :- Request] 77 | (p/initiate-authentication-handshake component request)) 78 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014 JUXT LTD. 2 | 3 | (defproject bolt "0.6.0-SNAPSHOT" 4 | :description "An integrated security system for applications built on component" 5 | :url "https://github.com/juxt/bolt" 6 | :license {:name "The MIT License" 7 | :url "http://opensource.org/licenses/MIT"} 8 | :dependencies 9 | [[org.clojure/tools.logging "0.3.1"] 10 | 11 | [juxt.modular/bidi "0.9.2" :exclusions [bidi]] 12 | [juxt.modular/ring "0.5.2"] 13 | [juxt.modular/email "0.0.1"] 14 | [juxt.modular/co-dependency "0.2.0"] 15 | 16 | [prismatic/schema "0.4.2"] 17 | [prismatic/plumbing "0.4.2"] 18 | 19 | ;; Required for OAuth2/OpenID-Connect support 20 | [cheshire "5.4.0"] 21 | 22 | [bidi "1.18.10" :exclusions [ring/ring-core 23 | org.clojure/tools.reader]] 24 | 25 | ;; Doesn't work with clojure 1.7.0-beta2 26 | #_[camel-snake-kebab "0.3.1" 27 | :exclusions [com.keminglabs/cljx]] 28 | 29 | ;; We should probably replace clj-jwt with buddy 30 | [clj-jwt "0.0.8" 31 | ;; Important we exclude bc here otherwise get an 32 | ;; this exception: 33 | ;; 34 | ;; class 35 | ;; "org.bouncycastle.crypto.digests.SHA3Digest"'s 36 | ;; signer information does not match signer 37 | ;; information of other classes in the same package 38 | :exclusions [clj-time 39 | org.bouncycastle/bcprov-jdk15]] 40 | 41 | [buddy "0.5.1"] 42 | [yada "0.4.2"] 43 | [clj-time "0.9.0"] 44 | 45 | ;; Possibly needed old dependencies 46 | #_[ring/ring-core "1.3.2" 47 | :exclusions [org.clojure/tools.reader 48 | clj-time]] 49 | #_[org.clojure/tools.reader "0.8.13"] 50 | #_[clj-time "0.9.0"] 51 | #_[juxt.modular/http-kit "0.5.3"] 52 | #_[hiccup "1.0.5"] 53 | #_[liberator "0.12.0"]] 54 | 55 | :repl-options {:init-ns user 56 | :welcome (println "Type (dev) to start")} 57 | 58 | :profiles 59 | {:dev {:main bolt.dev.main 60 | :dependencies 61 | [[org.clojure/clojure "1.7.0-beta2"] 62 | 63 | [org.clojure/tools.logging "0.2.6"] 64 | [ch.qos.logback/logback-classic "1.0.7" 65 | :exclusions [org.slf4j/slf4j-api]] 66 | [org.slf4j/jul-to-slf4j "1.7.2"] 67 | [org.slf4j/jcl-over-slf4j "1.7.2"] 68 | [org.slf4j/log4j-over-slf4j "1.7.2"] 69 | 70 | [com.stuartsierra/component "0.2.2"] 71 | [org.clojure/tools.namespace "0.2.5"] 72 | 73 | [markdown-clj "0.9.62"] 74 | 75 | [juxt.modular/aleph "0.0.8" :exclusions [manifold]] 76 | [juxt.modular/bidi "0.9.2" :exclusions [bidi]] 77 | [juxt.modular/clostache "0.6.3"] 78 | [juxt.modular/co-dependency "0.2.0"] 79 | [juxt.modular/maker "0.5.0"] 80 | [juxt.modular/test "0.1.0"] 81 | [juxt.modular/template "0.6.3"] 82 | 83 | [org.webjars/jquery "2.1.3"] 84 | [org.webjars/bootstrap "3.3.2"] 85 | ] 86 | :source-paths ["dev/src"] 87 | :resource-paths ["dev/resources"]}}) 88 | -------------------------------------------------------------------------------- /src/bolt/user/protocols.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user.protocols) 4 | 5 | (defprotocol UserStore 6 | "A store for users that doesn't involve password hashing" 7 | 8 | (check-create-user [_ user] 9 | "Check the user can be created. E.g. username and/or email doesn't 10 | already exist, otherwise render an error page, all required fields in 11 | correct format, etc. All fields are sent apart from the password. This 12 | is exposed as a function so that it be used in form validation prior to 13 | submission. May return a (manifold) deferred for async.") 14 | 15 | (create-user! [_ user] 16 | "Create the user. Implementations should call, and return the result 17 | of, create-user-error? prior to adding the user to storage. Returns the 18 | created user, perhaps with more information than the parameter 19 | given. May return the user wrapped in a (manifold) deferred for async.") 20 | 21 | (find-user [_ id] 22 | "Find the user identified by id") 23 | 24 | (update-user! [_ id user] 25 | "Update the user identified by id with the new details provided") 26 | 27 | (delete-user! [_ id] 28 | "Delete the user identified by id") 29 | 30 | (verify-email! [_ email] 31 | "Verify that the given email exists")) 32 | 33 | (defprotocol UserAuthenticator 34 | (authenticate-user [_ user credential] 35 | "Return truthy if credential is valid")) 36 | 37 | (defprotocol UserPasswordHasher 38 | (hash-password [_ password] 39 | "Return a hash of a plain-text password, as a string")) 40 | 41 | (defprotocol LoginFormRenderer 42 | (render-login-form [_ req model] 43 | "Render a login from from the data contained in the given model")) 44 | 45 | ;; TODO: Split up into separate protocols for modularity 46 | (defprotocol UserFormRenderer 47 | (render-signup-form [_ req model] 48 | "Return the HTML that will be used to display the sign up form for a 49 | new user to complete.") 50 | 51 | (render-welcome [_ req model] 52 | "Return the HTML that will be used to welcome the new user.") 53 | 54 | (render-welcome-email-message [_ model] 55 | "Return the text that will be emailed to a new user who has just 56 | signing up. The text should include the given link. Return nil for 57 | no email message.") 58 | 59 | (render-email-verified [_ req model] 60 | "Return the HTML that will be used to thank the user for verifying 61 | their email address.") 62 | 63 | (render-reset-password-request-form [_ req model] 64 | "Return the HTML that will be used to capture the email address of a user that wishes to reset their password.") 65 | 66 | (render-reset-password-email-message [_ model] 67 | "Return the text that will be emailed to a user, including a link 68 | that allows them to reset their password.") 69 | 70 | (render-reset-password-link-sent-response [_ req model] 71 | "Return the HTML that will inform a user that an email has been sent") 72 | 73 | (render-password-reset-form [_ req model] 74 | "Return the HTML that will be used to display the password reset 75 | form that will capture the new password. ") 76 | 77 | (render-password-changed-response [_ req model] 78 | "Return the HTML that will be used to tell the user that their 79 | password has been changed.") 80 | ) 81 | 82 | (defprotocol ErrorRenderer 83 | (render-error-response [_ req model] 84 | "If anything fails, gracefully report to the user.")) 85 | -------------------------------------------------------------------------------- /src/bolt/session/cookie_session_store.clj: -------------------------------------------------------------------------------- 1 | ;; TODO: This is misnamed. The cookie only contains a UUID, which keys 2 | ;; into a token store containing the material. 3 | 4 | (ns bolt.session.cookie-session-store 5 | (:require 6 | [clojure.tools.logging :refer :all] 7 | [com.stuartsierra.component :refer (using)] 8 | [bolt.session :refer (session)] 9 | [bolt.session.protocols :refer (SessionStore)] 10 | 11 | [bolt.authentication.protocols :refer (RequestAuthenticator)] 12 | [bolt.token-store :refer (get-token-by-id merge-token! create-token! purge-token!)] 13 | [ring.middleware.cookies :refer (cookies-request cookies-response)] 14 | [schema.core :as s] 15 | [plumbing.core :refer (<-)])) 16 | 17 | (defn ->cookie [session] 18 | {:value (:bolt/token-id session) 19 | :expires (.toGMTString 20 | (doto (new java.util.Date) 21 | (.setTime (.getTime (:bolt/expiry session))))) 22 | :path "/"}) 23 | 24 | ;(doto (new java.util.Date) (.setTime (.getTime (:c {:c (new java.util.Date)})))) 25 | (def delete-cookie 26 | {:value "" 27 | :expires (.toGMTString (java.util.Date. 70 0 1)) 28 | :path "/"}) 29 | 30 | (defn cookies-response-with-session [response id-cookie session] 31 | ;; Use of cookies-response mean it is non-destructive - existing 32 | ;; cookies are preserved (but existing :cookies entries are not) 33 | (cookies-response 34 | (merge-with merge response 35 | {:cookies {id-cookie (->cookie session)}}))) 36 | 37 | ;; This record satisfies SessionStore, indexed by a specific 38 | ;; cookie-id. This design allows us to encapsulate the cookie-id, rather 39 | ;; than have to pass it through numerous function calls. 40 | (defrecord CookieSessionStore [cookie-id token-store] 41 | SessionStore 42 | (session [component request] 43 | ;; In case the underlying token store accepts nils, we should avoid 44 | ;; retrieving a nil-indexed token, so we wrap in a 'when-let'. 45 | (when-let [tokid (-> request cookies-request :cookies (get cookie-id) :value)] 46 | (get-token-by-id token-store tokid))) 47 | 48 | (assoc-session-data! [component request m] 49 | (when-let [tokid (-> request cookies-request :cookies (get cookie-id) :value)] 50 | (merge-token! token-store tokid m))) 51 | 52 | (respond-with-new-session! [component request data response] 53 | ;; TODO Create a HMAC'd identifier, not just a random UUID that 54 | ;; could be predicted and therefore allow session forgery. 55 | 56 | (let [id (str (java.util.UUID/randomUUID)) 57 | token (create-token! token-store id data)] 58 | (debugf "Creating new session (%s) cookie %s tied to token %s" (:token-type token-store) id token) 59 | (cookies-response-with-session response cookie-id token))) 60 | 61 | (respond-close-session! [component request response] 62 | (when-let [tokid (-> request cookies-request :cookies (get cookie-id) :value)] 63 | (purge-token! token-store tokid)) 64 | (cookies-response 65 | (merge-with merge response 66 | {:cookies {cookie-id delete-cookie}}))) 67 | 68 | RequestAuthenticator 69 | (authenticate [component req] 70 | (session component req))) 71 | 72 | (def new-cookie-session-store-schema {:cookie-id s/Str}) 73 | 74 | (defn new-cookie-session-store [& {:as opts}] 75 | (->> opts 76 | (merge {:cookie-id "session-id"}) 77 | (s/validate new-cookie-session-store-schema) 78 | map->CookieSessionStore 79 | (<- (using [:token-store])))) 80 | -------------------------------------------------------------------------------- /dev/resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Styles */ 2 | 3 | html,body { 4 | font-family: 'Source Sans Pro', sans-serif; 5 | font-size: 1.1em; 6 | background: #f8f8f8; 7 | line-height: 1.1em; 8 | } 9 | 10 | a { 11 | text-decoration: underline; 12 | } 13 | 14 | li { 15 | margin: 0.5em 0; 16 | } 17 | 18 | h2 { 19 | font-weight: bold; 20 | } 21 | 22 | h1 { 23 | font-size: 5em; 24 | padding: 0.5em 0; 25 | } 26 | 27 | h2 { 28 | padding-bottom: 0.7em; 29 | } 30 | 31 | div.chapter { 32 | margin-top: 1.7em; 33 | border-top: 2px solid #888; 34 | } 35 | 36 | h3 { 37 | margin-top: 1.7em; 38 | margin-bottom: 1em; 39 | } 40 | 41 | h4 { 42 | font-weight: bold; 43 | } 44 | 45 | p { 46 | margin: 1.5em 0; 47 | text-align: justify; 48 | } 49 | 50 | .status { 51 | font-family: monospace; 52 | } 53 | 54 | .headers { 55 | font-family: monospace; 56 | white-space: pre; 57 | } 58 | 59 | pre { 60 | padding: 0; 61 | } 62 | 63 | pre code { 64 | font-size: 12pt; 65 | } 66 | 67 | code { 68 | font-size: 12pt; 69 | font-family: monospace; 70 | } 71 | 72 | .body { 73 | font-family: monospace; 74 | width: 100%; 75 | } 76 | 77 | div.example { 78 | border: 5px solid #99f; 79 | background: #aaf; 80 | } 81 | 82 | div.example table { 83 | background: black; 84 | color: #65b042; 85 | } 86 | 87 | div.example textarea { 88 | border: 0px solid black; 89 | background: black; 90 | } 91 | 92 | div.note { 93 | border: 5px solid #cc5; 94 | background: #dd6; 95 | } 96 | 97 | div.example { 98 | padding: 10px 10px; 99 | margin: 40px 5%; 100 | } 101 | 102 | div.note { 103 | padding: 10px 10px; 104 | margin: 40px 30% 0 0; 105 | } 106 | 107 | .example h3, .note h3 { 108 | font-family: 'Source Sans Pro', sans-serif; 109 | margin-top: 0px; 110 | margin-bottom: 10px; 111 | } 112 | 113 | .note p, .example p { 114 | margin: 0.5em 0; 115 | } 116 | 117 | .note a { 118 | color: #a00; 119 | text-decoration: underline; 120 | } 121 | 122 | 123 | code.shell { 124 | color: #0f0; 125 | font-weight: normal; 126 | } 127 | 128 | code.shell span { 129 | color: #0f0; 130 | } 131 | 132 | code.shell:before { 133 | color: green; 134 | font-weight: normal; 135 | content: "$ "; 136 | } 137 | 138 | 139 | .green { 140 | color: #3a3; 141 | } 142 | 143 | .red { 144 | color: #f33; 145 | font-weight: bold; 146 | } 147 | 148 | code { 149 | color: inherit; 150 | font-weight: bold; 151 | background: inherit; 152 | } 153 | 154 | /* The yada logo */ 155 | span.yada { 156 | font-family: RochesterYada, sans-serif; 157 | font-size: 1.25em; 158 | /* Pad ever so slightly on the left */ 159 | padding-left: 0.08em; 160 | } 161 | 162 | code span.yada, b span.yada { 163 | font-family: inherit; 164 | font-size: inherit; 165 | } 166 | 167 | span.bolt { 168 | font-style: normal; 169 | font-weight: bold; 170 | } 171 | 172 | code span.bolt, b span.bolt { 173 | font-family: inherit; 174 | font-size: inherit; 175 | } 176 | 177 | blockquote { 178 | border-left: 5px solid #aaa; 179 | } 180 | 181 | .mastfoot { 182 | line-height: 1.5em; 183 | } 184 | 185 | .juxt-logo { 186 | width: 80px; 187 | } 188 | 189 | .mastfoot hr { 190 | border-color: #888; 191 | border-width: 2px; 192 | } 193 | 194 | .juxt-logo-small { 195 | width: 52px; 196 | } 197 | 198 | div.warning { 199 | border: 3px solid #600; 200 | background: #a00; 201 | margin-bottom: 1em; 202 | padding: 10px; 203 | } 204 | 205 | div.warning p { 206 | color: white; 207 | font-weight: bold; 208 | margin: 0; 209 | } 210 | 211 | div.warning p:before { 212 | content: "WARNING: "; 213 | } 214 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/example1.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.dev.example1 2 | (:require 3 | [clojure.pprint :refer (pprint)] 4 | [clojure.tools.logging :refer :all] 5 | [bidi.bidi :refer (RouteProvider tag)] 6 | [bidi.ring :refer (redirect)] 7 | [com.stuartsierra.component :refer (using Lifecycle)] 8 | [bolt.user.protocols :refer (LoginFormRenderer UserFormRenderer)] 9 | [bolt.session :refer (session)] 10 | [bolt.session.protocols :refer (SessionStore)] 11 | [bolt.user.protocols :refer (UserStore UserPasswordHasher)] 12 | [bolt.user :refer (create-user! hash-password)] 13 | [modular.bidi :refer (as-request-handler path-for)] 14 | [modular.component.co-dependency :refer (co-using)] 15 | [modular.template :as template :refer (render-template template-model Templater TemplateModel)] 16 | [clojure.java.io :as io] 17 | [clojure.string :as str] 18 | [schema.core :as s] 19 | [schema.utils :refer [class-schema]] 20 | [hiccup.core :refer (html)] 21 | [bolt.dev.view :refer (page-body page)])) 22 | 23 | (s/defrecord Example 24 | [templater :- (s/protocol Templater) 25 | session-store :- (s/protocol SessionStore) 26 | user-store :- (s/protocol UserStore) 27 | password-hasher :- (s/protocol UserPasswordHasher) 28 | uri-context :- String 29 | ;; Co-dependencies 30 | *template-model 31 | *router] 32 | 33 | Lifecycle 34 | (start [component] 35 | (s/validate (class-schema (type component)) component) 36 | 37 | ;; Add some users 38 | (println "Create alice" 39 | (create-user! 40 | user-store {:email "alice@example.org" 41 | :password (hash-password password-hasher "wonderland") 42 | :roles #{:superuser}})) 43 | (println "Create bob" 44 | (create-user! 45 | user-store {:email "bob@example.org" 46 | :password (hash-password password-hasher "bob") 47 | :roles #{:user}})) 48 | 49 | (println "email user store is now " (-> user-store :storage :ref deref)) 50 | component) 51 | 52 | (stop [component] component) 53 | 54 | RouteProvider 55 | (routes [_] 56 | [(str uri-context) 57 | {"/index.html" 58 | (-> (page "templates/example1/index.html.mustache" templater *template-model *router session-store) 59 | (tag ::index)) 60 | "/protected.html" 61 | (-> (page "templates/example1/protected.html.mustache" templater *template-model *router session-store) 62 | (tag ::protected)) 63 | "" (redirect ::index) 64 | "/" (redirect ::index)}]) 65 | 66 | UserFormRenderer 67 | (render-signup-form 68 | [component req model] 69 | (page-body 70 | templater "templates/dialog.html.mustache" 71 | (merge (template/template-model @*template-model req) 72 | {:title (:title model) 73 | :form 74 | (html 75 | [:form {:action (-> model :form :action) 76 | :method (-> model :form :method)} 77 | ;; Hidden fields 78 | (for [{:keys [name value type]} (-> model :form :fields) 79 | :when (= type "hidden")] 80 | [:input {:type type :name name :value value}]) 81 | [:div 82 | [:label {:for "email"} "Email"] 83 | [:input#email {:type :text :name "user"}]] 84 | [:div 85 | [:label {:for "password"} "Password"] 86 | [:input#password {:type :password :name "password"}]] 87 | [:div 88 | [:input.submit {:type "submit" :value "Sign up"}] 89 | ]])}))) 90 | 91 | TemplateModel 92 | (template-model 93 | [component req] 94 | (let [login-href 95 | (when-let [path (path-for @*router :bolt.user.login/login-form)] 96 | (str path "?post_login_redirect=" (path-for @*router ::index))) 97 | logout-href 98 | (when-let [path (path-for @*router :bolt.user.login/logout)] 99 | (str path "?post_logout_redirect=" (path-for @*router ::index)))] 100 | 101 | (assert login-href "No href to login. Check system dependencies.") 102 | (assert logout-href "No href to logout. Check system dependencies.") 103 | 104 | {:menu [] 105 | :login-href login-href 106 | :logout-href logout-href 107 | }))) 108 | 109 | (defn new-example [& {:as args}] 110 | (-> 111 | (->> args 112 | (merge {}) 113 | (s/validate {:uri-context s/Str}) 114 | (map->Example)) 115 | (using [:templater :session-store :user-store]) 116 | (co-using [:router :template-model]))) 117 | -------------------------------------------------------------------------------- /src/bolt/user/login.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user.login 4 | (:require 5 | [clojure.tools.logging :refer :all] 6 | [clojure.string :as string] 7 | [bolt.user.protocols :as p] 8 | [bolt.authentication.protocols :refer (RequestAuthenticator AuthenticationHandshake)] 9 | [bolt.session :refer (session assoc-session-data! respond-with-new-session! respond-close-session!)] 10 | [bolt.session.protocols :refer (SessionStore)] 11 | [bolt.user :refer (find-user render-login-form authenticate-user)] 12 | [bolt.util :refer (as-query-string uri-with-qs Request wrap-schema-validation keywordize-form)] 13 | [bidi.bidi :refer (RouteProvider tag)] 14 | [modular.bidi :refer (path-for)] 15 | [ring.util.response :refer (redirect redirect-after-post)] 16 | [ring.middleware.params :refer (params-request)] 17 | [plumbing.core :refer (<-)] 18 | [com.stuartsierra.component :refer (Lifecycle using)] 19 | [schema.core :as s] 20 | [modular.component.co-dependency :refer (co-using)]) 21 | (:import (java.net URLEncoder))) 22 | 23 | (defn email? [s] 24 | (re-matches #".+@.+" s)) 25 | 26 | (defrecord Login [user-store user-authenticator session-store renderer uri-context *router] 27 | Lifecycle 28 | (start [component] 29 | (s/validate 30 | {:user-store (s/protocol p/UserStore) 31 | :user-authenticator (s/protocol p/UserAuthenticator) 32 | :session-store (s/protocol SessionStore) 33 | :renderer (s/protocol p/LoginFormRenderer) 34 | :uri-context s/Str 35 | :*router s/Any ;; you can't get specific protocol of a codependency in start time 36 | } 37 | component)) 38 | (stop [component] component) 39 | 40 | AuthenticationHandshake 41 | (initiate-authentication-handshake [component req] 42 | (assert (:routes @*router)) 43 | (if-let [p (path-for @*router ::login-form)] 44 | (let [loc (str p (as-query-string {"post_login_redirect" (URLEncoder/encode (uri-with-qs req))}))] 45 | (debugf "Redirecting to %s" loc) 46 | (redirect loc)) 47 | (throw (ex-info "No path to login form" {})))) 48 | 49 | RequestAuthenticator 50 | (authenticate [component req] 51 | (session session-store req)) 52 | 53 | RouteProvider 54 | (routes [component] 55 | [uri-context 56 | {"/login" 57 | {:get 58 | (-> 59 | (fn [req] 60 | (let [qparams (-> req params-request :query-params) 61 | post-login-redirect (get qparams "post_login_redirect")] 62 | 63 | {:status 200 64 | :body (render-login-form 65 | renderer req 66 | (merge 67 | {:form (merge {:method :post 68 | :action (path-for @*router ::process-login-attempt)}) 69 | :login-failed? (Boolean/valueOf (get qparams "login_failed"))} 70 | (when post-login-redirect {:post-login-redirect post-login-redirect})))})) 71 | wrap-schema-validation 72 | (tag ::login-form) 73 | ) 74 | 75 | :post 76 | (-> 77 | (fn [req] 78 | (let [form (-> req params-request :form-params keywordize-form) 79 | _ (infof "Form is %s" form) 80 | id (some-> (get form :user) string/trim) 81 | password (get form :password) 82 | post-login-redirect (get form :post-login-redirect) 83 | 84 | session (session session-store req) 85 | user (find-user user-store id) 86 | authentication (when user (authenticate-user user-authenticator user {:password password}))] 87 | 88 | (if (and user authentication) 89 | ;; Login successful! 90 | (do 91 | (debugf "Login successful!") 92 | (respond-with-new-session! 93 | session-store req 94 | {:bolt/user user 95 | ;; It might be useful to store the results of the 96 | ;; authentication (which could be signed) 97 | :bolt/authentication authentication} 98 | (if post-login-redirect 99 | (redirect-after-post post-login-redirect) 100 | {:status 200 :body "Login successful"}))) 101 | 102 | ;; Login failed! 103 | (redirect-after-post 104 | (str (path-for @*router ::login-form) 105 | ;; We must be careful to add back the query string 106 | (as-query-string 107 | (merge 108 | (when post-login-redirect 109 | {"post_login_redirect" (URLEncoder/encode post-login-redirect)}) 110 | ;; Add a login_failed to help with indicating the failure to the user. 111 | {"login_failed" true} 112 | ))))))) 113 | wrap-schema-validation 114 | (tag ::process-login-attempt) 115 | )} 116 | 117 | "/logout" 118 | {:get 119 | (-> 120 | (fn [req] 121 | (let [qparams (-> req params-request :query-params) 122 | post-logout-redirect (get qparams "post_logout_redirect")] 123 | (respond-close-session! session-store req (redirect post-logout-redirect)))) 124 | wrap-schema-validation 125 | (tag ::logout) 126 | )} 127 | }])) 128 | 129 | (defn new-login [& {:as opts}] 130 | (->> opts 131 | (merge {:uri-context ""}) 132 | (s/validate {:uri-context s/Str}) 133 | map->Login 134 | (<- (using [:user-store :user-authenticator :session-store :renderer])) 135 | (<- (co-using [:router])) 136 | )) 137 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/user_guide.clj: -------------------------------------------------------------------------------- 1 | (ns bolt.dev.user-guide 2 | (:require 3 | [bidi.bidi :refer (tag RouteProvider alts)] 4 | [bidi.ring :refer (redirect)] 5 | [cheshire.core :as json] 6 | [clojure.java.io :as io] 7 | [clojure.tools.logging :refer :all] 8 | [clojure.pprint :refer (pprint *print-right-margin*)] 9 | [clojure.string :as str] 10 | [clojure.walk :refer (postwalk)] 11 | [clojure.xml :refer (parse)] 12 | [com.stuartsierra.component :refer (using Lifecycle)] 13 | [hiccup.core :refer (h html) :rename {h escape-html}] 14 | [markdown.core :refer (md-to-html-string)] 15 | [modular.bidi :refer (path-for)] 16 | [modular.template :as template :refer (render-template)] 17 | [modular.component.co-dependency :refer (co-using)] 18 | [yada.yada :refer (yada)])) 19 | 20 | (defn emit-element 21 | ;; An alternative emit-element that doesn't cause newlines to be 22 | ;; inserted around punctuation. 23 | [e] 24 | (if (instance? String e) 25 | (print e) 26 | (do 27 | (print (str "<" (name (:tag e)))) 28 | (when (:attrs e) 29 | (doseq [attr (:attrs e)] 30 | (print (str " " (name (key attr)) 31 | "='" 32 | (let [v (val attr)] (if (coll? v) (apply str v) v)) 33 | "'")))) 34 | (if (:content e) 35 | (do 36 | (print ">") 37 | (if (instance? String (:content e)) 38 | (print (:content e)) 39 | (doseq [c (:content e)] 40 | (emit-element c))) 41 | (print (str ""))) 42 | (print "/>"))))) 43 | 44 | (defn basename [r] 45 | (last (str/split (.getName (type r)) #"\."))) 46 | 47 | (defn enclose [^String s] 48 | (format "
%s
" s)) 49 | 50 | (defn xml-parse [^String s] 51 | (parse (io/input-stream (.getBytes s)))) 52 | 53 | (defn get-source [] 54 | (xml-parse (enclose (md-to-html-string 55 | (slurp (io/resource "user-guide.md")))))) 56 | 57 | (defn title [s] 58 | (letfn [(lower [x] 59 | (if (#{"as" "and" "of" "for"} 60 | (str/lower-case x)) (str/lower-case x) x)) 61 | (part [x] 62 | (if (Character/isDigit (char (first x))) 63 | (format "(part %s)" x) 64 | x 65 | ) 66 | )] 67 | (->> (re-seq #"[A-Z1-9][a-z]*" s) 68 | (map lower) 69 | (map part) 70 | (str/join " ")))) 71 | 72 | (defn chapter [c] 73 | (str/replace (apply str c) #"\s+" "")) 74 | 75 | (defn ->meth 76 | [m] 77 | (str/upper-case (name m))) 78 | 79 | (defn extract-chapters [xml] 80 | (let [xf (comp (filter #(= (:tag %) :h2)) (mapcat :content))] 81 | (map str (sequence xf (xml-seq xml))))) 82 | 83 | (defn link [r] 84 | (last (str/split (.getName (type r)) #"\."))) 85 | 86 | (defn toc [xml dropno] 87 | {:tag :ul 88 | :attrs nil 89 | :content (vec 90 | (for [ch (drop dropno (extract-chapters xml))] 91 | {:tag :li 92 | :attrs nil 93 | :content [{:tag :a 94 | :attrs {:href (str "#" (chapter ch))} 95 | :content [ch]}]}))}) 96 | 97 | (defn post-process-doc [user-guide xml config] 98 | (postwalk 99 | (fn [{:keys [tag attrs content] :as el}] 100 | (cond 101 | (= tag :h2) 102 | ;; Add an HTML anchor to each chapter, for hrefs in 103 | ;; table-of-contents and elsewhere 104 | {:tag :div 105 | :attrs {:class "chapter"} 106 | :content [{:tag :a :attrs {:name (chapter content)} :content []} el]} 107 | 108 | (= tag :include) 109 | ;; Include some content 110 | {:tag :div 111 | :attrs {:class (:type attrs)} 112 | :content [{:tag :a :attrs {:name (:ref attrs)} :content []} 113 | (some-> (format "includes/%s.md" (:ref attrs)) 114 | io/resource slurp md-to-html-string enclose xml-parse)]} 115 | 116 | (= tag :toc) 117 | (toc xml (Integer/parseInt (:drop attrs))) 118 | 119 | (and (= tag :p) (= (count content) 1) (= (:tag (first content)) :div)) 120 | ;; Raise divs in paragraphs. 121 | (first content) 122 | 123 | (= tag :code) 124 | (update-in el [:content] (fn [x] (map (fn [y] (if (string? y) (str/trim y) y)) x))) 125 | 126 | :otherwise el)) 127 | xml)) 128 | 129 | (defn post-process-body 130 | "Some whitespace reduction" 131 | [s prefix] 132 | (assert prefix) 133 | (-> s 134 | (str/replace #"\{\{prefix\}\}" prefix) 135 | (str/replace #"\{\{(.+)\}\}" #(or (System/getProperty (last %)) "")) 136 | (str/replace #"

\s*

" "") 137 | (str/replace #"(bolt)(?![-/])" "bolt") 138 | )) 139 | 140 | (defn body [{:keys [*router templater] :as user-guide} doc {:keys [prefix]}] 141 | (render-template 142 | templater 143 | "templates/page.html.mustache" 144 | {:content 145 | (-> (with-out-str (emit-element doc)) 146 | (post-process-body prefix) 147 | ) 148 | :scripts []})) 149 | 150 | (defrecord UserGuide [*router templater prefix ext-prefix] 151 | Lifecycle 152 | (start [component] 153 | (infof "Starting user-guide") 154 | (assert prefix) 155 | (let [xbody (get-source)] 156 | (assoc 157 | component 158 | :start-time (java.util.Date.) 159 | :xbody xbody))) 160 | (stop [component] component) 161 | 162 | RouteProvider 163 | (routes [component] 164 | (let [xbody (:xbody component)] 165 | ["/user-guide" 166 | [[".html" 167 | (-> 168 | (yada 169 | :body {"text/html" 170 | (fn [ctx] 171 | (let [config {:prefix prefix :ext-prefix ext-prefix}] 172 | (body component (post-process-doc component xbody config) config)))}) 173 | (tag ::user-guide))] 174 | ]]))) 175 | 176 | (defn new-user-guide [& {:as opts}] 177 | (-> (->> opts 178 | (merge {}) 179 | map->UserGuide) 180 | (using [:templater]) 181 | (co-using [:router]))) 182 | -------------------------------------------------------------------------------- /src/bolt/user/reset_password.clj: -------------------------------------------------------------------------------- 1 | ; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user.reset-password 4 | (:require 5 | [clojure.tools.logging :refer :all] 6 | [com.stuartsierra.component :as component :refer (using)] 7 | [bolt.session.protocols :refer (session assoc-session-data! respond-with-new-session!)] 8 | [bolt.user.protocols :refer (LoginFormRenderer UserFormRenderer)] 9 | [bolt.user :refer (render-reset-password-request-form render-reset-password-email-message render-reset-password-link-sent-response render-password-reset-form render-password-changed-response hash-password)] 10 | [bolt.token-store :refer (create-token! get-token-by-id purge-token!)] 11 | [bolt.util :refer (absolute-uri absolute-prefix as-query-string wrap-schema-validation)] 12 | [hiccup.core :refer (html)] 13 | [bidi.bidi :refer (RouteProvider tag)] 14 | [modular.bidi :refer (path-for)] 15 | [ring.middleware.params :refer (params-request)] 16 | [ring.util.response :refer (response redirect)] 17 | [schema.core :as s] 18 | [plumbing.core :refer (<-)] 19 | [modular.email :refer (send-email!)] 20 | [modular.component.co-dependency :refer (co-using)] 21 | )) 22 | 23 | (defrecord ResetPassword [emailer renderer session-store user-store verification-code-store fields-reset fields-confirm-password password-verifier uri-context *router] 24 | RouteProvider 25 | (routes [this] 26 | [uri-context 27 | {"request-reset-password" 28 | { 29 | ;; GET: show the find by user form to reset the password 30 | :get 31 | (-> 32 | (fn [req] 33 | {:status 200 34 | :body (render-reset-password-request-form 35 | renderer req 36 | {:form {:method :post 37 | :action (path-for @*router ::process-reset-password-request) 38 | :fields fields-reset}})}) 39 | 40 | wrap-schema-validation 41 | (tag ::request-reset-password-form)) 42 | 43 | ;; POST: find a user by email and send email with reset-password-link 44 | :post 45 | (-> 46 | (fn [req] 47 | (let [form (-> req params-request :form-params) 48 | email (get form "email")] 49 | ;; TODO: We need to look up the user by email, rather than any 50 | ;; other id. This poses a challenge, because we don't have a 51 | ;; protocol for doing this yet 52 | (if-let [user (throw (ex-info "TODO" {})) #_(get-user-by-email user-store email)] 53 | (let [code (str (java.util.UUID/randomUUID))] 54 | (debugf "Found user: %s" user) 55 | (create-token! verification-code-store code user) 56 | (send-email! 57 | emailer (merge 58 | {:to email} 59 | (render-reset-password-email-message 60 | renderer 61 | {:link (str 62 | (absolute-prefix req) 63 | (path-for @*router ::reset-password-form) 64 | (as-query-string {"code" code}))}))) 65 | (->> 66 | (response 67 | (render-reset-password-link-sent-response 68 | renderer req {:email email})) 69 | (respond-with-new-session! session-store req {}))) 70 | 71 | (redirect (format "%s?unknown-email=%s" 72 | (path-for @*router ::request-reset-password-form) 73 | email))))) 74 | wrap-schema-validation 75 | (tag ::process-reset-password-request) 76 | )} 77 | 78 | "reset-password" 79 | {:get 80 | (-> 81 | (fn [req] 82 | (let [params (-> req params-request :params)] 83 | (let [code (get params "code") 84 | token (get-token-by-id (:verification-code-store this) code)] 85 | (if token 86 | {:status 200 87 | :body (render-password-reset-form 88 | renderer req 89 | (merge 90 | {:form {:method :post 91 | :action (path-for @*router ::process-password-reset) 92 | ;; add hidden field 93 | :fields (conj fields-confirm-password 94 | {:name "code" :type "hidden" :value code})}} 95 | token))} 96 | ;; TODO: This is unhelpful - render a 400 error message instead. 97 | {:status 404 :body "Not found"} 98 | )))) 99 | wrap-schema-validation 100 | (tag ::reset-password-form)) 101 | 102 | :post 103 | (-> 104 | (fn [req] 105 | (let [form (-> req params-request :form-params) 106 | token-id (get form "code") 107 | token (get-token-by-id (:verification-code-store this) token-id) 108 | pw (get form "new_pw")] 109 | 110 | (if token 111 | (do 112 | (infof "Reseting password for user %s" (:uid token)) 113 | ;; TODO How to update the user password here? 114 | (throw (ex-info "TODO")) 115 | #_(set-user-password-hash! 116 | user-store 117 | (:uid token) 118 | (hash-password user-password-hasher pw)) 119 | (purge-token! (:verification-code-store this) token-id) 120 | (response (render-password-changed-response renderer req {}))) 121 | 122 | ;; TODO: Here's where we must display an error, via calling a protocol 123 | {:status 400 :body (format "ERROR: no such token for code: %s" token-id)}))) 124 | wrap-schema-validation 125 | (tag ::process-password-reset) 126 | )}}])) 127 | 128 | (def new-reset-password-schema 129 | {:uri-context s/Str}) 130 | 131 | (defn new-reset-password [& {:as opts}] 132 | (->> opts 133 | (merge 134 | {:uri-context "/"}) 135 | (s/validate new-reset-password-schema) 136 | map->ResetPassword 137 | (<- (using [:user-store :session-store :renderer 138 | :verification-code-store :password-verifier :emailer])) 139 | (<- (co-using [:router])))) 140 | -------------------------------------------------------------------------------- /src/bolt/user.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user 4 | (:require 5 | [bolt.user.protocols :as p] 6 | [bolt.util :refer (Request)] 7 | [schema.core :as s] 8 | [modular.email :refer (EmailAddress EmailMessage)] 9 | )) 10 | 11 | ;; UserStore API 12 | 13 | (s/defschema User "A user" 14 | {s/Keyword s/Any}) 15 | 16 | (s/defn check-create-user 17 | [component :- (s/protocol p/UserStore) 18 | user :- User] 19 | (p/check-create-user component user)) 20 | 21 | (s/defn create-user! 22 | [component :- (s/protocol p/UserStore) 23 | user :- User] 24 | (p/create-user! component user)) 25 | 26 | (s/defn find-user :- (s/maybe User) 27 | [component :- (s/protocol p/UserStore) 28 | id :- s/Str] 29 | (p/find-user component id)) 30 | 31 | (s/defn update-user! :- nil 32 | [component :- (s/protocol p/UserStore) 33 | id :- s/Str 34 | user :- User] 35 | (p/update-user! component id user)) 36 | 37 | (s/defn delete-user! :- nil 38 | [component :- (s/protocol p/UserStore) 39 | id :- s/Str] 40 | (p/delete-user! component id)) 41 | 42 | (s/defn verify-email! :- nil 43 | [component :- (s/protocol p/UserStore) 44 | email :- s/Str] 45 | (p/verify-email! component email)) 46 | 47 | ;; UserAuthenticator API 48 | 49 | (s/defn authenticate-user :- s/Any 50 | [component :- (s/protocol p/UserAuthenticator) 51 | user :- User 52 | evidence :- {s/Keyword s/Str}] 53 | (p/authenticate-user component user evidence)) 54 | 55 | (s/defn hash-password :- s/Str 56 | [component :- (s/protocol p/UserPasswordHasher) 57 | password :- s/Str] 58 | (p/hash-password component password)) 59 | 60 | ;; Login form renderer API 61 | 62 | #_(s/defschema FormField 63 | {:name s/Str 64 | (s/optional-key :label) s/Str 65 | (s/optional-key :placeholder) s/Str 66 | (s/optional-key :type) s/Str 67 | (s/optional-key :value) s/Str}) 68 | 69 | (s/defschema Form 70 | {:method s/Keyword 71 | :action s/Str}) 72 | 73 | ;; TODO When schema validating requests, with {:headers s/Any} I get the following :- 74 | ;; 75 | ;; java.lang.ClassCastException: java.lang.Class cannot be cast to clojure.lang.IFn 76 | ;; at aleph.http.core.NettyRequest.entryAt(core.clj:146) 77 | ;; at clojure.lang.RT.find(RT.java:807) 78 | ;; at clojure.core$find.invoke(core.clj:1465) 79 | ;; at schema.core$map_walker$fn__10092$fn__10095.invoke(core.clj:741) 80 | ;; at schema.core$map_walker$fn__10092.invoke(core.clj:728) 81 | ;; at schema.core$eval10188$fn__10189$fn__10206.invoke(core.clj:840) 82 | ;; at clojure.core$comp$fn__4492.invoke(core.clj:2436) 83 | ;; at bolt.user$eval46172$render_login_form__46175.invoke(user.clj:81) 84 | ;; at bolt.user.login.Login$fn__47684.invoke(login.clj:69) 85 | ;; at bolt.util$wrap_schema_validation$fn__45964.invoke(util.clj:66) 86 | ;; at bidi.ring$eval11505$fn__11506.invoke(ring.clj:22) 87 | ;; at bidi.ring$eval11484$fn__11485$G__11475__11494.invoke(ring.clj:13) 88 | ;; at bidi.ring$make_handler$fn__11510.invoke(ring.clj:35) 89 | ;; at clojure.core$some_fn$sp2__6921.invoke(core.clj:7144) 90 | ;; at aleph.http.server$wrap_stream__GT_input_stream$fn__31772.invoke(server.clj:392) 91 | ;; at aleph.http.server$handle_request$fn__31696$f__12499__auto____31697.invoke(server.clj:153) 92 | ;; at clojure.lang.AFn.run(AFn.java:22) 93 | ;; at io.aleph.dirigiste.Executor$Worker$1.run(Executor.java:49) 94 | ;; at java.lang.Thread.run(Thread.java:745) 95 | ;; 96 | ;; NettyRequest is defined with potemkin/def-derived-map. Perhaps this is not compatible with how schema validates. 97 | ;; Workaround is to remove request validation. 98 | 99 | (s/defn render-login-form :- s/Str 100 | [component :- (s/protocol p/LoginFormRenderer) 101 | req ;; :- Request 102 | model :- {:form Form 103 | (s/optional-key :login-failed?) s/Bool 104 | (s/optional-key :post-login-redirect) s/Str}] 105 | (p/render-login-form component req model)) 106 | 107 | ;; User form renderer API 108 | 109 | (s/defn render-signup-form :- s/Str 110 | [component :- (s/protocol p/UserFormRenderer) 111 | req ;; :- Request 112 | model :- {:form Form}] 113 | (p/render-signup-form component req model)) 114 | 115 | (s/defn render-welcome :- s/Str 116 | [component :- (s/protocol p/UserFormRenderer) 117 | req ;; :- Request 118 | model :- {}] 119 | (p/render-welcome component req model)) 120 | 121 | (s/defn render-welcome-email-message :- EmailMessage 122 | [component :- (s/protocol p/UserFormRenderer) 123 | model :- {:email-verification-link s/Str}] 124 | (p/render-welcome-email-message component model)) 125 | 126 | (s/defn render-email-verified :- s/Str 127 | [component :- (s/protocol p/UserFormRenderer) 128 | req ;; :- Request 129 | model :- {:email EmailAddress 130 | s/Keyword s/Any}] 131 | (p/render-email-verified component req model)) 132 | 133 | (s/defn render-reset-password-request-form :- s/Str 134 | [component :- (s/protocol p/UserFormRenderer) 135 | req ;; :- Request 136 | model :- {:form Form 137 | (s/optional-key :email-failed?) s/Bool}] 138 | (p/render-reset-password-request-form component req model)) 139 | 140 | (s/defn render-reset-password-email-message :- EmailMessage 141 | [component :- (s/protocol p/UserFormRenderer) 142 | model :- {:link s/Str}] 143 | (p/render-reset-password-email-message component model)) 144 | 145 | (s/defn render-reset-password-link-sent-response :- s/Str 146 | [component :- (s/protocol p/UserFormRenderer) 147 | req ;; :- Request 148 | model :- {:email EmailAddress}] 149 | (p/render-reset-password-link-sent-response component req model)) 150 | 151 | (s/defn render-password-reset-form :- s/Str 152 | [component :- (s/protocol p/UserFormRenderer) 153 | req ;; :- Request 154 | model :- {:form Form 155 | s/Keyword s/Any}] 156 | (p/render-password-reset-form component req model)) 157 | 158 | (s/defn render-password-changed-response :- s/Str 159 | [component :- (s/protocol p/UserFormRenderer) 160 | req ;; :- Request 161 | model :- {}] 162 | (p/render-password-changed-response component req model)) 163 | 164 | ;; Error form renderer API 165 | 166 | (s/defn render-error :- s/Str 167 | [component :- (s/protocol p/ErrorRenderer) 168 | req ;; :- Request 169 | model :- {:error-type (s/enum :user-already-exists) 170 | s/Keyword s/Any}] 171 | (p/render-error-response component req model)) 172 | -------------------------------------------------------------------------------- /dev/resources/public/img/juxt-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 17 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /dev/src/bolt/dev/system.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2015, JUXT LTD. 2 | 3 | (ns bolt.dev.system 4 | "Components and their dependency relationships" 5 | (:refer-clojure :exclude (read)) 6 | (:require 7 | [clojure.java.io :as io] 8 | [clojure.tools.reader :refer (read)] 9 | [clojure.string :as str] 10 | [clojure.tools.reader.reader-types :refer (indexing-push-back-reader)] 11 | [com.stuartsierra.component :refer (system-map system-using using)] 12 | 13 | [bolt.dev.website :refer (new-website)] 14 | [bolt.dev.user-guide :refer (new-user-guide)] 15 | [bolt.dev.database :refer (new-database)] 16 | [bolt.dev.example1 :refer (new-example)] 17 | [bolt.dev.login-form :refer (new-login-form)] 18 | 19 | [bolt.session.cookie-session-store :refer (new-cookie-session-store)] 20 | [bolt.token-store.atom-backed-store :refer (new-atom-backed-token-store)] 21 | [bolt.user.login :refer (new-login)] 22 | [bolt.user.email-user-store :refer (new-email-user-store)] 23 | [bolt.user.buddy-user-authenticator :refer (new-buddy-user-authenticator)] 24 | [bolt.storage.atom-storage :refer (new-atom-storage)] 25 | 26 | [modular.maker :refer (make)] 27 | [modular.bidi :refer (new-router new-web-resources new-archived-web-resources new-redirect)] 28 | [modular.clostache :refer (new-clostache-templater)] 29 | [modular.template :refer (new-aggregate-template-model)] 30 | [modular.aleph :refer (new-webserver)] 31 | [modular.component.co-dependency :refer (co-using system-co-using)])) 32 | 33 | (defn ^:private read-file 34 | [f] 35 | (read 36 | ;; This indexing-push-back-reader gives better information if the 37 | ;; file is misconfigured. 38 | (indexing-push-back-reader 39 | (java.io.PushbackReader. (io/reader f))))) 40 | 41 | (defn ^:private config-from 42 | [f] 43 | (if (.exists f) 44 | (read-file f) 45 | {})) 46 | 47 | (defn ^:private user-config 48 | [] 49 | (config-from (io/file (System/getProperty "user.home") ".bolt.edn"))) 50 | 51 | (defn ^:private config-from-classpath 52 | [] 53 | (if-let [res (io/resource "bolt.edn")] 54 | (config-from (io/file res)) 55 | {})) 56 | 57 | (defn config 58 | "Return a map of the static configuration used in the component 59 | constructors." 60 | [] 61 | (merge (config-from-classpath) 62 | (user-config))) 63 | 64 | (defn database-components [system config] 65 | (assoc system 66 | :database 67 | (-> 68 | (make new-database config) 69 | (using [])))) 70 | 71 | (defn website-components [system config] 72 | (assoc 73 | system 74 | :clostache-templater (make new-clostache-templater config) 75 | :user-guide (make new-user-guide config 76 | :prefix "http://localhost:8084") 77 | :website (make new-website config) 78 | :jquery (make new-web-resources config 79 | :key :jquery 80 | :uri-context "/jquery" 81 | :resource-prefix "META-INF/resources/webjars/jquery/2.1.3") 82 | :bootstrap (make new-web-resources config 83 | :key :bootstrap 84 | :uri-context "/bootstrap" 85 | :resource-prefix "META-INF/resources/webjars/bootstrap/3.3.2") 86 | :web-resources (make new-web-resources config 87 | :uri-context "/static" 88 | :resource-prefix "public") 89 | :highlight-js-resources 90 | (make new-archived-web-resources config :archive (io/resource "highlight.zip") :uri-context "/hljs/") 91 | )) 92 | 93 | (defn router-components [system config] 94 | (assoc system 95 | :router 96 | (make new-router config))) 97 | 98 | (defn http-server-components [system config] 99 | (assoc system 100 | :http-server 101 | (make new-webserver config 102 | :port 8084))) 103 | 104 | (defn example1-components [system config] 105 | (let [uri-context "/example1"] 106 | (assoc 107 | system 108 | :example1 (bolt.dev.example1/new-example :uri-context uri-context) 109 | :example1#session-store (new-cookie-session-store) 110 | :example1#token-store (new-atom-backed-token-store) 111 | :example1#login (new-login :uri-context uri-context) 112 | :example1#email-user-store (new-email-user-store) 113 | :example1#buddy-user-authenticator (new-buddy-user-authenticator) 114 | :example1#atom-storage (new-atom-storage) 115 | :example1#login-form (new-login-form) 116 | :example1#template-model (new-aggregate-template-model) 117 | ))) 118 | 119 | (defn new-system-map 120 | [config] 121 | (apply system-map 122 | (apply concat 123 | (-> {} 124 | (database-components config) 125 | (website-components config) 126 | (router-components config) 127 | (http-server-components config) 128 | (example1-components config) 129 | (assoc :redirect (new-redirect :from "/" :to :bolt.dev.website/index)) 130 | )))) 131 | 132 | (defn new-dependency-map 133 | [] 134 | {:http-server {:request-handler :router} 135 | :router [:user-guide 136 | :website 137 | :jquery :bootstrap 138 | :web-resources 139 | :highlight-js-resources 140 | :redirect 141 | :example1 142 | :example1#login] 143 | :user-guide {:templater :clostache-templater} 144 | :website {:templater :clostache-templater} 145 | 146 | :example1 {:templater :clostache-templater 147 | :session-store :example1#session-store 148 | :user-store :example1#email-user-store 149 | :password-hasher :example1#buddy-user-authenticator} 150 | :example1#template-model [:example1] 151 | 152 | ;; These are the components to support security (login, etc.) 153 | :example1#session-store {:token-store :example1#token-store} 154 | 155 | :example1#login {:user-store :example1#email-user-store 156 | :user-authenticator :example1#buddy-user-authenticator 157 | :session-store :example1#session-store 158 | :renderer :example1#login-form} 159 | 160 | :example1#login-form {:templater :clostache-templater} 161 | 162 | :example1#email-user-store {:storage :example1#atom-storage}}) 163 | 164 | (defn new-co-dependency-map 165 | [] 166 | {:website {:router :router} 167 | :user-guide {:router :router} 168 | :example1 {:router :router 169 | :template-model :example1#template-model} 170 | :example1#login-form {:template-model :example1#template-model 171 | :router :router} 172 | }) 173 | 174 | (defn new-production-system 175 | "Create the production system" 176 | [] 177 | (-> (new-system-map (config)) 178 | (system-using (new-dependency-map)) 179 | (system-co-using (new-co-dependency-map)))) 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bolt 2 | 3 | > “Building secure Clojure web applications needs to be easier, and 4 | > requires integrated security frameworks - not standalone libraries!” – 5 | > John P. Hackworth, [Clojure web security is worse than you think](https://hackworth.be/2014/03/26/clojure-web-security-is-worse-than-you-think/) 6 | 7 | An integrated security system for Clojure applications based on a set of 8 | components written to the specifications of Stuart Sierra's 9 | [component](https://github.com/stuartsierra/component). 10 | 11 | Bolt has an ambitious goal: to separate all security-related concerns 12 | from Clojure-based applications so that they can be implemented as 13 | pluggable components. 14 | 15 | ## Terms 16 | 17 | The precise meanings of the terms component, system-map and system are 18 | those in [component](https://github.com/stuartsierra/component). In 19 | summary, a _component_ is a map of data, usually implemented as a record 20 | with associated protocols specifying functions for start/stop and 21 | others. A _system_ is a set of these components, with the inclusion of 22 | declared dependency references into each component. 23 | 24 | In addition, bolt uses the following terms :- 25 | 26 | * username - a user's short identifier, for example: __bob__ 27 | * email - a user's email address 28 | * user - a map, containing entries that distinguish and describe a user 29 | 30 | ## Discussion 31 | 32 | Bolt provides an _integrated system_ of components, rather than requiring developers 33 | to roll their own from smaller libraries. 34 | 35 | Functionality can be customised by interchanging components, providing 36 | necessary flexibility for bespoke Clojure applications. 37 | 38 | Nevertheless, 'out-of-the-box' defaults should provide good security, on 39 | par with other languages and frameworks. That is what is currently 40 | missing in the Clojure landscape and the gap that bolt aims to fill. 41 | 42 | ### Differences with Friend 43 | 44 | The key difference between bolt and Friend is that bolt is designed 45 | for use with [Component](https://github.com/stuartsierra/component) 46 | based applications. 47 | 48 | Bolt is designed specifically for modular applications, where 49 | functionality can be added through the addition of extra components. 50 | 51 | Stuart Sierra's component library provides a balanced, elegant and 52 | "essential" foundation for bringing all these parts together into a 53 | single system, so it's a natural fit for this problem. It is also 54 | straight-forward to decompose (and therefore reason about) the system 55 | (by understanding the role that each component plays). This is an 56 | important property of any security system - if the design is difficult 57 | to comprehend but 'just works' or works 'like magic' then it limits the 58 | number of people who can understand it and point out potential 59 | weaknesses. 60 | 61 | To provide flexibility, bolt fully embraces and consistently adopts 62 | _protocol polymorphism_ within Clojure, enabled by Stuart's 63 | approach. This will not to everyone's taste. Alternatives, such as the 64 | use of dynamic vars, are wholly avoided. Functional programming is a 65 | beautiful thing in the small, but presents practical challenges at 66 | scale. Polymorphism is one of the cornerstones of object orientation 67 | worth stealing. 68 | 69 | ### Should you use bolt? 70 | 71 | Ultimately, whether bolt is right for you will depend on how you build 72 | your Clojure web applications. For smaller applications with a single 73 | set of Compojure routes, Friend is a better choice. 74 | 75 | For larger applications, especially those with multiple modules and 76 | using [Liberator](http://clojure-liberator.github.io/liberator/) or 77 | [yada](https://github.com/juxt/yada) to provide a fuller REST API, bolt 78 | should be a good fit. 79 | 80 | ## Name change 81 | 82 | Bolt was formerly named Cylon. 83 | 84 | ## Limitations 85 | 86 | Bolt is not suitable for production systems until it reaches 87 | version 1.0, which will indicate that bolt has been deployed into 88 | production elsewhere and has undergone thorough peer review. 89 | 90 | ## Join in the conversation 91 | 92 | Join our Google group bolt-discuss@googlegroups.com for discussion 93 | about how to improve bolt. 94 | 95 | ## References 96 | 97 | https://hackworth.be/2014/03/26/clojure-web-security-is-worse-than-you-think/ 98 | https://github.com/dhruvchandna/ring-secure-headers 99 | https://github.com/weavejester/ring-anti-forgery 100 | 101 | ## Acknowledgements 102 | 103 | Aaron Bedra's seminal ClojureWest talk in 2014 – 104 | http://www.youtube.com/watch?v=CBL59w7fXw4 - this was the inspiration 105 | behind bolt. 106 | 107 | [Mastodon C](http://www.mastodonc.com/) for sponsoring the development 108 | on bolt, and using it in their kixi projects 109 | [kixi.hecuba](https://github.com/MastodonC/kixi.hecuba) and 110 | [kixi.stentor](https://github.com/MastodonC/kixi.stentor) 111 | 112 | Also, to Neale Swinnerton [@sw1nn](https://twitter.com/sw1nn) for the 113 | original work in adopting Stuart's component library and showing how to 114 | migrate [Jig](https://github.com/juxt/jig) components to it. 115 | 116 | [Yodit Stanton](https://github.com/yods) and the rest of the 117 | [opensensors.io](https://opensensors.io) team for putting up with the 118 | original Cylon updates (Cylon being the former name of bolt) and being 119 | the first adopters of the OAuth2 features. 120 | 121 | [Juan Antonio Ruz](https://github.com/tangrammar) for designing and 122 | developing the TOTP two-factor authentication support. Additionally Juan 123 | conducted the background research and co-authored the OAuth2 support, 124 | and many other aspects of the project. Also for providing a public 125 | example of how to use bolt. 126 | 127 | [Martin Trojer](https://github.com/martintrojer) and others from 128 | [JUXT](https://github.com/juxt) for a continual stream of 129 | thought-provoking ideas and good advice. 130 | 131 | [Andrey Antukh](https://github.com/niwibe) for suggestions about 132 | integration with [Buddy](https://github.com/niwibe/buddy). 133 | 134 | ## Copyright & License 135 | 136 | The MIT License (MIT) 137 | 138 | Copyright © 2014 JUXT LTD. 139 | 140 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 141 | 142 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 143 | 144 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 145 | -------------------------------------------------------------------------------- /src/bolt/user/signup.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.user.signup 4 | (:require 5 | [bidi.bidi :refer (RouteProvider tag)] 6 | [modular.email :refer (send-email!)] 7 | [modular.email.protocols :refer (Emailer)] 8 | [clojure.tools.logging :refer :all] 9 | [bolt.util :refer (absolute-prefix as-query-string wrap-schema-validation keywordize-form)] 10 | [bolt.session :refer (session respond-with-new-session! assoc-session-data!)] 11 | [bolt.session.protocols :refer (SessionStore)] 12 | [bolt.token-store :refer (create-token! get-token-by-id)] 13 | [bolt.token-store.protocols :refer (TokenStore)] 14 | [com.stuartsierra.component :as component :refer (Lifecycle using)] 15 | [modular.bidi :refer (path-for)] 16 | [hiccup.core :refer (html)] 17 | [ring.middleware.params :refer (params-request)] 18 | [ring.middleware.cookies :refer (cookies-response wrap-cookies)] 19 | [ring.util.response :refer (response redirect redirect-after-post)] 20 | [bolt.user :refer (create-user! verify-email! render-signup-form render-welcome-email-message render-email-verified render-error hash-password)] 21 | [bolt.user.protocols :refer (UserStore UserPasswordHasher UserAuthenticator UserFormRenderer ErrorRenderer)] 22 | [schema.core :as s] 23 | [plumbing.core :refer (<-)] 24 | [modular.component.co-dependency :refer (co-using)] 25 | [yada.yada :refer (yada)] 26 | ) 27 | (:import (clojure.lang ExceptionInfo))) 28 | 29 | (defn make-verification-link [req code email *router] 30 | (let [values ((juxt (comp name :scheme) :server-name :server-port) req) 31 | verify-user-email-path (path-for @*router ::verify-user-email)] 32 | (apply format "%s://%s:%d%s?code=%s&email=%s" (conj values verify-user-email-path code email)))) 33 | 34 | (def new-signup-schema 35 | {:uri-context s/Str 36 | ;; TODO: Ensure this can't be hijacked, see section on 'Preventing 37 | ;; redirect attacks' here: 38 | ;; http://rundis.github.io/blog/2015/buddy_auth_part2.html 39 | (s/optional-key :post-signup-redirect) s/Str}) 40 | 41 | (defrecord Signup [user-store user-password-hasher session-store verification-code-store renderer emailer uri-context *router] 42 | Lifecycle 43 | (start [component] 44 | (s/validate (merge 45 | new-signup-schema 46 | {:user-store (s/protocol UserStore) 47 | :user-password-hasher (s/protocol UserPasswordHasher) 48 | :session-store (s/protocol SessionStore) 49 | :verification-code-store (s/protocol TokenStore) 50 | :renderer (s/protocol UserFormRenderer) 51 | :emailer (s/protocol Emailer) 52 | :*router s/Any ;; you can't get specific protocol of a codependency in start time 53 | }) 54 | component)) 55 | (stop [component] component) 56 | 57 | RouteProvider 58 | (routes [component] 59 | [uri-context 60 | {"signup" 61 | {:get 62 | (-> 63 | (fn [req] 64 | (let [resp (response (render-signup-form 65 | renderer req 66 | {:title "Sign up" 67 | :form {:method :post 68 | :action (path-for @*router ::POST-signup-form)}}))] 69 | 70 | (if-not (session session-store req) 71 | ;; We create an empty session. This is because the POST 72 | ;; handler requires that a session exists within which it can 73 | ;; store the identity on a successful login 74 | ;; (revisit: the comment above is wrong, the POST handler can 75 | ;; create the session) 76 | (respond-with-new-session! session-store req {} resp) 77 | resp))) 78 | (tag ::GET-signup-form)) 79 | 80 | :post 81 | (-> 82 | (fn [req] 83 | 84 | (let [form (-> req params-request :form-params) 85 | ;;uid (get form "user-id") 86 | password (get form "password") 87 | 88 | ;;name (get form "name") 89 | 90 | ;; We remove the plain-text password to avoid sending it 91 | ;; through the API 92 | user (-> form (dissoc "password") keywordize-form) 93 | 94 | ;; Create the user 95 | ;; TODO: Watch out, create-user! can return a manifold deferred 96 | user (create-user! user-store 97 | (assoc user :password-hash (hash-password user-password-hasher password))) 98 | ] 99 | 100 | (when (:error user) (throw (ex-info "Failed to create user" user))) 101 | 102 | 103 | ;; TODO: Check the password meets policy constraints (length, etc.) 104 | 105 | ;; Check the user can be created. E.g. uid and/or email 106 | ;; doesn't already exist, otherwise render an error page, 107 | ;; all required fields in correct format, etc. 108 | ;; (create-user-error? user-store user) 109 | 110 | ;; Send the email to the user now! 111 | (when emailer 112 | (let [code (str (java.util.UUID/randomUUID))] 113 | (create-token! 114 | verification-code-store code 115 | {:bolt/user user}) 116 | 117 | (when-let [email (:email user)] 118 | (send-email! 119 | emailer 120 | (merge {:to email} 121 | (render-welcome-email-message 122 | renderer 123 | {:email-verification-link 124 | (str 125 | (absolute-prefix req) 126 | (path-for @*router ::verify-user-email) 127 | (as-query-string {"code" code}))})))))) 128 | 129 | ;; Create a session that contains the secret-key 130 | ;; (assoc-session-data! session-store req {:bolt/subject-identifier uid :name name}) 131 | 132 | (respond-with-new-session! 133 | session-store req 134 | user ; keep logged in after signup 135 | (if-let [loc (or (get form "post_signup_redirect") 136 | (:post-signup-redirect component))] 137 | (redirect-after-post loc) 138 | (response (format "Thank you, %s, for signing up" user)))))) 139 | 140 | wrap-schema-validation 141 | (tag ::POST-signup-form) 142 | )} 143 | 144 | "verify-email" 145 | {:get 146 | (-> 147 | (fn [req] 148 | (let [params (-> req params-request :params)] 149 | (let [token-id (get params "code") 150 | token (get-token-by-id (:verification-code-store component) token-id)] 151 | (if-let [uid (:bolt/subject-identifier token)] 152 | (do 153 | (verify-email! user-store uid) 154 | (response (render-email-verified renderer req token))) 155 | {:status 400 :body (format "No known verification code: %s" token-id)})))) 156 | wrap-schema-validation 157 | (tag ::verify-user-email))} 158 | }])) 159 | 160 | (defn new-signup [& {:as opts}] 161 | (->> opts 162 | (merge {:uri-context "/"}) 163 | (s/validate new-signup-schema) 164 | map->Signup 165 | (<- (using [:user-store 166 | :user-password-hasher 167 | :session-store 168 | :verification-code-store 169 | :renderer 170 | :emailer])) 171 | (<- (co-using [:router])))) 172 | -------------------------------------------------------------------------------- /src/bolt/oauth/client/web_client.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.oauth.client.web-client 4 | (:require 5 | [bidi.bidi :refer (RouteProvider tag)] 6 | [cheshire.core :refer (encode decode-stream)] 7 | [clj-jwt.core :refer (to-str jwt sign str->jwt verify encoded-claims)] 8 | [clojure.java.io :as io] 9 | [clojure.set :refer (union)] 10 | [clojure.tools.logging :refer :all] 11 | [com.stuartsierra.component :as component] 12 | [bolt.authentication.protocols :refer (RequestAuthenticator)] 13 | [bolt.oauth.client :refer (AccessTokenGrantee solicit-access-token expired?)] 14 | [bolt.oauth.registry :refer (register-client)] 15 | [bolt.oauth.registry.protocols :refer (ClientRegistry)] 16 | [bolt.oauth.encoding :refer (encode-scope decode-scope)] 17 | [bolt.session :refer (session respond-with-new-session! assoc-session-data! respond-close-session!)] 18 | [bolt.token-store :refer (create-token! get-token-by-id purge-token!)] 19 | [bolt.util :refer (as-set absolute-uri as-www-form-urlencoded as-query-string)] 20 | [aleph.http :as http] 21 | [ring.middleware.cookies :refer (wrap-cookies cookies-request cookies-response)] 22 | [ring.middleware.params :refer (wrap-params)] 23 | [ring.util.response :refer (redirect)] 24 | [schema.core :as s] 25 | )) 26 | 27 | (defrecord WebClient [access-token-uri 28 | end-session-endpoint post-logout-redirect-uri 29 | session-store state-store 30 | client-registry 31 | uri-context] 32 | component/Lifecycle 33 | (start [this] 34 | ;; If there's an :client-registry dependency, use it to 35 | ;; register this app. 36 | (debugf "Starting web client and registering with the client-registry: %s" client-registry) 37 | 38 | (if client-registry 39 | (let [{:keys [client-id client-secret]} 40 | (s/with-fn-validation 41 | (register-client 42 | client-registry 43 | (select-keys 44 | this 45 | [:client-id 46 | :client-secret 47 | :application-name 48 | :homepage-uri 49 | :description 50 | :redirection-uri 51 | :required-scopes 52 | :requires-user-acceptance? 53 | ])))] 54 | ;; In case these are generated 55 | (assoc this :client-id client-id :client-secret client-secret)) 56 | 57 | ;; If no app registry, make sure we can standalone as an app. 58 | (s/validate {:client-id s/Str 59 | :client-secret s/Str 60 | s/Keyword s/Any} this))) 61 | (stop [this] this) 62 | 63 | RouteProvider 64 | (routes [this] 65 | [uri-context 66 | {"grant" 67 | {:get 68 | (-> 69 | ;; Used by the authorization server to return responses containing 70 | ;; authorization credentials to the client via the resource owner 71 | ;; user-agent. 72 | (fn [req] 73 | (let [params (:query-params req) 74 | state (get params "state") 75 | tok (get-token-by-id state-store state)] 76 | 77 | (if (nil? tok) 78 | (let [session (session session-store req) 79 | original-uri (:bolt/original-uri session)] 80 | (if original-uri 81 | (redirect original-uri) 82 | {:status 400 :body "Unexpected user state"})) 83 | 84 | (let [code (get params "code") 85 | 86 | _ (infof "Exchanging code (%s) for access token via %s" code access-token-uri) 87 | 88 | ;; Exchange the code for an access token 89 | ;; This is a blocking operation. We elect to wait for 90 | ;; the response. In a future version we might go fully 91 | ;; async. 92 | at-resp 93 | @(http/post access-token-uri 94 | {:headers {"content-type" "application/x-www-form-urlencoded"} 95 | ;; Exchange the code for an access token - application/x-www-form-urlencoded format 96 | ;; 2.3.1: "Including the client credentials in the 97 | ;; request-body using the two parameters is NOT 98 | ;; RECOMMENDED and SHOULD be limited to clients 99 | ;; unable to directly utilize the HTTP Basic 100 | ;; authentication scheme (or other password-based 101 | ;; HTTP authentication schemes)." 102 | 103 | ;; TODO Support Basic Authentication in preference 104 | ;; to client secrets. 105 | 106 | ;; 4.1.3. Access Token Request redirect_uri 107 | ;; REQUIRED, if the "redirect_uri" parameter was 108 | ;; included in the authorization request as 109 | ;; described in Section 4.1.1, and their values 110 | ;; MUST be identical. 111 | 112 | ;; TODO: Better if we could construct this string 113 | ;; with the help of some utility function. 114 | 115 | :body (as-www-form-urlencoded 116 | {"grant_type" "authorization_code" 117 | "code" code 118 | "client_id" (:client-id this) 119 | "client_secret" (:client-secret this)})} 120 | 121 | ;; TODO Arguably we need better error handling here 122 | #(if (:error %) 123 | (do 124 | (errorf "Failed to get token from %s, response was %s" access-token-uri %) 125 | %) 126 | (update-in % [:body] (if-let [decode-fn (:decode-server-response-fn this)] 127 | decode-fn 128 | ;; add default decode to bolt oauth server response 129 | (comp decode-stream io/reader)))))] 130 | 131 | (purge-token! state-store state) 132 | 133 | (if-let [error (:error at-resp)] 134 | {:status 403 135 | :body (format "Something went wrong: status of underlying request, error was %s" error)} 136 | 137 | (if (not= (:status at-resp) 200) 138 | {:status 403 139 | :body (format "Something went wrong: status of underlying request %s" (:status at-resp))} 140 | 141 | 142 | (let [original-uri (:bolt/original-uri (session session-store req)) 143 | access-token (get (:body at-resp) "access_token") 144 | 145 | ;; TODO If scope not there it is the same as 146 | ;; requested (see 5.1) 147 | scope (decode-scope (get (:body at-resp) "scope") (keyword? (first (:required-scopes this)))) 148 | 149 | id-token (-> (get (:body at-resp) "id_token") str->jwt)] 150 | (if (verify id-token "secret") 151 | (do 152 | (assert original-uri (str "Failed to get original-uri from session " (session session-store req))) 153 | 154 | (infof "Verified id_token: %s" id-token) 155 | (infof "Scope is %s" scope) 156 | (infof "Claims are %s" (:claims id-token)) 157 | 158 | (assoc-session-data! 159 | session-store req {:bolt/access-token access-token 160 | :bolt/scopes scope 161 | :bolt/open-id (-> id-token :claims) 162 | :bolt/subject-identifier (-> id-token :claims :sub)}) 163 | (redirect original-uri)))))))))) 164 | wrap-params 165 | (tag ::redirection-endpoint))} 166 | 167 | "logout" 168 | {:get 169 | (-> 170 | (fn [req] 171 | ;; http://openid.net/specs/openid-connect-session-1_0.html - chapter 5 172 | 173 | ;; "An RP can notify the OP that the End-User has logged 174 | ;; out of the site, and might want to log out of the OP 175 | ;; as well. In this case, the RP, after having logged 176 | ;; the End-User out of the RP, redirects the End-User's 177 | ;; User Agent to the OP's logout endpoint URL. This URL 178 | ;; is normally obtained via the end_session_endpoint 179 | ;; element of the OP's Discovery response, or may be 180 | ;; learned via other mechanisms." 181 | 182 | ;; post_logout_redirect_uri 183 | ;; OPTIONAL. URL to which the RP is requesting that the 184 | ;; End-User's User 185 | ;; Agent be redirected after a logout has been performed. The value MUST 186 | ;; have been previously registered with the OP, either using the 187 | ;; post_logout_redirect_uris Registration parameter or via another 188 | ;; mechanism. If supplied, the OP SHOULD honor this request following 189 | ;; the logout. 190 | 191 | ;; TODO Perhaps we need to redirect to a logout on the auth-server side, with a original-uri of location-after-logout 192 | 193 | (respond-close-session! 194 | session-store req 195 | (cond 196 | end-session-endpoint 197 | ;; "An RP can notify the OP that the End-User has logged out of the site" 198 | ;; If specified, add the OPTIONAL post_logout_redirect_uri query parameter 199 | (redirect (str end-session-endpoint (when post-logout-redirect-uri (str "?post_logout_redirect_uri=" post-logout-redirect-uri)))) 200 | 201 | ;; If there's only a post-logout-redirect-uri, then redirect to it 202 | post-logout-redirect-uri (redirect post-logout-redirect-uri) 203 | :otherwise {:status 200 :body "Logged out"} 204 | ))) 205 | (tag ::logout))}}]) 206 | 207 | AccessTokenGrantee 208 | (solicit-access-token [this req target-uri] 209 | (solicit-access-token this req target-uri [])) 210 | 211 | ;; RFC 6749 4.1. Authorization Code Grant (A) 212 | (solicit-access-token [this req target-uri scopes] 213 | (let [ 214 | state (str (java.util.UUID/randomUUID)) 215 | 216 | ;; 4.1.1. Authorization Request 217 | loc (str 218 | (:authorize-uri this) 219 | (as-query-string 220 | {"response_type" "code" ; REQUIRED 221 | "client_id" (:client-id this) ; REQUIRED 222 | ;; "redirect_uri" nil ; OPTIONAL (TODO) 223 | "scope" (encode-scope 224 | (union (as-set scopes) ; OPTIONAL 225 | (:required-scopes this))) 226 | "state" state ; RECOMMENDED to prevent CSRF 227 | }))] 228 | 229 | (create-token! state-store state {}) 230 | ;; We create a session 231 | (debugf "Creating session to store original uri of %s" target-uri) 232 | ;; We redirect to the (authorization) uri send the redirect response, but first 233 | 234 | ;; We need a session to store the original uri 235 | (respond-with-new-session! 236 | session-store req 237 | {:bolt/original-uri target-uri} 238 | (redirect loc)))) 239 | 240 | (expired? [_ req access-token] false) 241 | 242 | RequestAuthenticator 243 | (authenticate [component request] 244 | (session session-store request))) 245 | 246 | (defn new-web-client 247 | "Represents an OAuth2 client. This component provides all the web 248 | routes necessary to provide signup, login and password resets. It also 249 | acts as a RequestAuthorizer, which returns an OAuth2 access token from a 250 | call to authorized?" 251 | [& {:as opts}] 252 | (component/using 253 | (->> opts 254 | (merge {:requires-user-acceptance? true 255 | :uri-context "/"}) 256 | (s/validate {(s/optional-key :client-id) s/Str 257 | (s/optional-key :client-secret) s/Str 258 | :application-name s/Str 259 | :homepage-uri s/Str 260 | :redirection-uri s/Str 261 | (s/optional-key :post-logout-redirect-uri) s/Str 262 | 263 | :required-scopes (s/either #{s/Keyword} #{s/Str}) 264 | 265 | :authorize-uri s/Str 266 | :access-token-uri s/Str 267 | (s/optional-key :end-session-endpoint) s/Str 268 | 269 | :requires-user-acceptance? s/Bool 270 | :uri-context s/Str 271 | (s/optional-key :decode-server-response-fn) s/Any 272 | }) 273 | map->WebClient) 274 | [:session-store :state-store])) 275 | -------------------------------------------------------------------------------- /src/bolt/oauth/server/server.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014, JUXT LTD. All Rights Reserved. 2 | 3 | (ns bolt.oauth.server.server 4 | (require 5 | [clojure.set :as set] 6 | [clojure.string :as str] 7 | [clojure.tools.logging :refer :all] 8 | [bidi.bidi :refer (RouteProvider tag)] 9 | [modular.bidi :refer (path-for)] 10 | [cheshire.core :refer (encode)] 11 | [clj-jwt.core :refer (to-str sign jwt)] 12 | [clj-time.core :refer (now plus days)] 13 | [com.stuartsierra.component :as component :refer (Lifecycle)] 14 | [bolt.authentication :refer (authenticate initiate-authentication-handshake)] 15 | [bolt.authentication.protocols :refer (RequestAuthenticator AuthenticationHandshake)] 16 | [bolt.oauth.registry.protocols :refer (ClientRegistry)] 17 | [bolt.oauth.registry :refer (lookup-client)] 18 | [bolt.oauth.encoding :refer (decode-scope encode-scope)] 19 | [bolt.session :refer (session respond-with-new-session! assoc-session-data! respond-close-session!)] 20 | [bolt.session.protocols :refer (SessionStore)] 21 | [bolt.token-store :refer (create-token! get-token-by-id)] 22 | [bolt.token-store.protocols :refer (TokenStore)] 23 | [bolt.util :refer (as-query-string wrap-schema-validation md5)] 24 | [hiccup.core :refer (html h)] 25 | [plumbing.core :refer (<-)] 26 | [ring.middleware.cookies :refer (cookies-request)] 27 | [ring.middleware.cookies :refer (wrap-cookies cookies-request cookies-response)] 28 | [ring.middleware.params :refer (params-request)] 29 | [ring.util.response :refer (redirect)] 30 | [schema.core :as s] 31 | [modular.component.co-dependency :refer (co-using)])) 32 | 33 | (def new-authorization-server-schema 34 | {:scopes {s/Keyword {:description s/Str}} 35 | :store s/Any 36 | :iss s/Str ; uri actually, see openid-connect ch 2. 37 | :uri-context s/Str 38 | }) 39 | 40 | (defrecord AuthorizationServer [store scopes iss 41 | session-store 42 | access-token-store 43 | authentication-handshake 44 | client-registry 45 | uri-context 46 | *router] 47 | Lifecycle 48 | (start [component] 49 | ;; It is essential that the authentication-handshake has the same 50 | ;; session store as this component, otherwise we won't be 51 | ;; able to access the authentication when this handler is called again. 52 | (assert (= (:session-store authentication-handshake) session-store)) 53 | 54 | (s/validate 55 | (merge new-authorization-server-schema 56 | {:session-store (s/protocol SessionStore) 57 | :access-token-store (s/protocol TokenStore) 58 | :authentication-handshake (s/protocol AuthenticationHandshake) 59 | :client-registry (s/protocol ClientRegistry) 60 | :*router s/Any ; should be a promise 61 | }) 62 | component)) 63 | (stop [component] component) 64 | 65 | RouteProvider 66 | ;; TODO Implement RFC 6749 4.1.2.1 Error Response 67 | (routes [component] 68 | [uri-context 69 | {"authorize" 70 | {:get 71 | (-> 72 | (fn [req] 73 | 74 | ;; TODO We should validate the incoming response_type 75 | 76 | (let [authentication (authenticate authentication-handshake req)] 77 | (debugf "OAuth2 authorization server: Authorizing request. User authentication is %s" authentication) 78 | ;; Establish whether the user-agent is already authenticated. 79 | 80 | 81 | ;; If we aren't authenticated, we hand off to the 82 | ;; authentication process, which will honor an existing 83 | ;; session or create a new one if one doesn't already 84 | ;; exist. Since we want to remember certain details of this 85 | ;; authorization request, we elect to create the session 86 | ;; here. The authentication will return to this same handler. 87 | 88 | ;; We initiate an authentication, which will ALWAYS 89 | ;; create a new session, so we store important details 90 | ;; about this request for the return. We 91 | 92 | (if-not (:bolt/subject-identifier authentication) 93 | (initiate-authentication-handshake authentication-handshake req) 94 | 95 | ;; Else... The user is AUTHENTICATED (now), so we AUTHORIZE the CLIENT 96 | (let [{response-type "response_type" 97 | client-id "client_id" 98 | scopes-param "scope" 99 | state "state"} (-> req params-request :query-params) 100 | requested-scopes (decode-scope scopes-param (keyword? (first scopes)))] 101 | 102 | (case response-type 103 | "code" 104 | (let [code (str (java.util.UUID/randomUUID)) 105 | 106 | {:keys [redirection-uri application-name description 107 | requires-user-acceptance? required-scopes] :as client} 108 | (lookup-client (:client-registry component) client-id)] 109 | 110 | ;; Why do we do this? 111 | ;; you need to associate the user-data, scopes, redirect-uri with params... with the code 112 | ;; (assoc-session-data! session-store req {:code code}) 113 | 114 | ;; Remember the code for the possible exchange - TODO expire these 115 | (swap! store assoc 116 | code 117 | (merge 118 | {:created (java.util.Date.)} 119 | ;; This is for the OpenID-Connect JWT token that we will send with the access-token 120 | (select-keys authentication [:bolt/subject-identifier :bolt/user]))) 121 | 122 | ;; When a user permits a client, the client's scopes that they have accepted, are stored in the user preferences database 123 | ;; why? 124 | ;; because next time, we don't have to ask the user for their permission everytime they login 125 | ;; ok, i understand 126 | ;; however 127 | 128 | (debugf (if requires-user-acceptance? 129 | "App requires user acceptance" 130 | "App does not require user acceptance")) 131 | ;; Lookup the application - do we have at-least the client id? 132 | (if requires-user-acceptance? 133 | {:status 200 134 | :body (html [:body 135 | [:form {:method :post :action (path-for @*router ::permit)} 136 | [:h1 "Authorize application?"] 137 | [:p (format "An application (%s) is requesting to use your credentials" application-name)] 138 | [:h2 "Application description"] 139 | [:p description] 140 | [:h2 "Scope"] 141 | (for [s requested-scopes] 142 | (let [s (apply str (interpose "/" (remove nil? ((juxt namespace name) s))))] 143 | [:p [:label {:for s} s] [:input {:type "checkbox" :id s :name s :value s :checked true}]])) 144 | [:input {:type "submit"}]] 145 | ])} 146 | 147 | (do 148 | (debugf (format "App doesn't require user acceptance, granting scopes as required: [%s]" required-scopes)) 149 | (swap! store update-in [code] assoc :granted-scopes required-scopes) 150 | ;; 4.1.2: "If the resource owner grants the 151 | ;; access request, the authorization server 152 | ;; issues an authorization code and delivers it 153 | ;; to the client by adding the following 154 | ;; parameters to the query component of the 155 | ;; redirection URI" 156 | (debugf "Redirecting to redirection uri: %s" redirection-uri) 157 | 158 | (redirect 159 | (str redirection-uri 160 | (as-query-string 161 | {"code" code 162 | "state" state})))))) 163 | 164 | ;; Unknown response_type 165 | {:status 400 166 | :body (format "Bad response_type parameter: '%s'" response-type)}))))) 167 | 168 | wrap-schema-validation 169 | (tag ::authorization-endpoint))} 170 | 171 | ;; ::permit is called by ::authorization-endpoint above, and it assumes 172 | ;; various things are placed in the current session. It hasn't been 173 | ;; properly tested (and we know it won't work as currently written) 174 | ;; so treat as a stub for now. 175 | "permit-client" 176 | {:post 177 | (-> 178 | (fn [req] 179 | ;; TODO I'm worred about the fact we must ensure that the session 180 | ;; represents a true authenticated user 181 | (let [session (session session-store req) 182 | form-params (-> req params-request :form-params) 183 | ] 184 | (if (:bolt/subject-identifier session) 185 | (let [permitted-scopes (set (map 186 | (fn [x] (apply keyword (str/split x #"/"))) 187 | (keys form-params))) 188 | _ (debugf "permitted-scopes is %s" permitted-scopes) 189 | requested-scopes (:requested-scopes session) 190 | _ (debugf "requested-scopes is %s" requested-scopes) 191 | 192 | granted-scopes (set/intersection permitted-scopes requested-scopes) 193 | code (:code session) 194 | client-id (:client-id session) 195 | {:keys [redirection-uri] :as client} (lookup-client (:client-registry component) client-id) 196 | ] 197 | 198 | (debugf "Granting scopes: %s" granted-scopes) 199 | (swap! store update-in [code] assoc :granted-scopes granted-scopes) 200 | 201 | (redirect 202 | (format "%s?code=%s&state=%s" 203 | redirection-uri code (:state session))))))) 204 | (tag ::permit))} 205 | 206 | "access-token" 207 | {:post 208 | ;; RFC 6749 4.1 (D) - and this is the Token endpoint as described 209 | ;; in section 3 (Protocol Endpoints) 210 | 211 | ;; This is initiated by the client 212 | (-> 213 | (fn [req] 214 | (let [params (-> req params-request :form-params) 215 | code (get params "code") 216 | client-id (get params "client_id") 217 | client (lookup-client (:client-registry component) client-id)] 218 | 219 | ;; "When making the request, the client authenticates with 220 | ;; the authorization server." 221 | (if (not= (get params "client_secret") (:client-secret client)) 222 | {:status 403 :body "Client could not be authenticated"} 223 | 224 | (if-let [{sub :bolt/subject-identifier 225 | user :bolt/user 226 | granted-scopes :granted-scopes} 227 | (get @store code)] 228 | 229 | (let [access-token (str (java.util.UUID/randomUUID)) 230 | ;; See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken 231 | claim (merge {:iss iss 232 | :sub sub 233 | :aud client-id 234 | :exp (plus (now) (days 1)) ; expiry ; TODO unhardcode 235 | :iat (now)} 236 | (when-let [name (:name user)] {:name name}) 237 | (when-let [email (:email user)] 238 | {:email email 239 | :picture (str "https://www.gravatar.com/avatar/" (md5 email)) 240 | }))] 241 | 242 | (create-token! access-token-store 243 | access-token 244 | {:client-id client-id 245 | :bolt/subject-identifier sub 246 | :bolt/scopes granted-scopes}) 247 | 248 | ;; Store the access token 249 | (assoc-session-data! session-store req {:bolt/access-token access-token}) 250 | (infof "Claim is %s" claim) 251 | 252 | ;; 5.1 Successful Response 253 | 254 | ;; " The authorization server issues an access token 255 | ;; and optional refresh token, and constructs the 256 | ;; response by adding the following parameters to the 257 | ;; entity-body of the HTTP response with a 200 (OK) 258 | ;; status code:" 259 | 260 | (debugf "About to OK, granted scopes is %s (type is %s)" granted-scopes (type granted-scopes)) 261 | (respond-close-session! 262 | session-store req 263 | {:status 200 264 | :body (encode 265 | {"access_token" access-token 266 | "token_type" "Bearer" 267 | "expires_in" 3600 268 | ;; TODO Refresh token (optional) 269 | 270 | ;; 5.1 scope OPTIONAL only if 271 | ;; identical to scope requested by 272 | ;; client, otherwise required. In 273 | ;; this way, we pass back the scope 274 | ;; to the client. 275 | "scope" (encode-scope granted-scopes) 276 | 277 | ;; OpenID Connect ID Token 278 | "id_token" 279 | (-> claim 280 | jwt 281 | (sign :HS256 "secret") to-str) 282 | })})) 283 | {:status 400 284 | :body (format "Invalid request - unrecognized code: %s" code)})))) 285 | (tag ::token-endpoint))}}])) 286 | 287 | (defn new-authorization-server [& {:as opts}] 288 | (->> opts 289 | (merge {:store (atom {}) 290 | :uri-context "/login/oauth/"}) 291 | (s/validate new-authorization-server-schema) 292 | map->AuthorizationServer 293 | (<- (component/using 294 | [:access-token-store 295 | :session-store 296 | :client-registry 297 | :authentication-handshake])) 298 | (<- (co-using [:router])))) 299 | --------------------------------------------------------------------------------