├── 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 |
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 "" (name (:tag e)) ">")))
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 |
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 |
--------------------------------------------------------------------------------