OpenID Implementation #194

Open
wants to merge 19 commits into
from
Commits
Jump to file or symbol
Failed to load files and symbols.
+194 −90
Diff settings

Always

Just for now

View
@@ -17,6 +17,7 @@
[clj-config "0.1.0"]
[incanter/incanter-core "1.2.3"]
[incanter/incanter-charts "1.2.3"]
+ [org.pretendcow/clj-openid "0.0.1-SNAPSHOT"]
[commons-lang "2.6"]
[org.apache.commons/commons-email "1.2"]]
:dev-dependencies [[lein-ring "0.4.5"]
View
@@ -1,19 +1,23 @@
(ns foreclojure.login
(:require [sandbar.stateful-session :as session]
- [ring.util.response :as response])
+ [ring.util.response :as response]
+ [clj-openid.core :as openid]
+ [clj-openid.helpers :as helpers])
(:import [org.jasypt.util.password StrongPasswordEncryptor])
(:use [hiccup.form-helpers :only [form-to label text-field password-field check-box]]
- [foreclojure.utils :only [from-mongo flash-error flash-msg form-row assuming send-email login-url]]
+ [foreclojure.utils :only [from-mongo flash-error flash-msg form-row assuming send-email login-url get-user]]
[foreclojure.template :only [def-page content-page]]
[foreclojure.messages :only [err-msg]]
[compojure.core :only [defroutes GET POST]]
[useful.map :only [keyed]]
[clojail.core :only [thunk-timeout]]
[clojure.stacktrace :only [print-cause-trace]]
- [somnium.congomongo :only [update! fetch-one]]))
+ [somnium.congomongo :only [update! fetch-one destroy!]]))
(def password-reset-url "https://www.4clojure.com/settings")
+(def openid-callback-url "http://www.4clojure.com/openid-callback")
+
(def login-box
(form-to [:post "/login"]
[:table
@@ -25,29 +29,41 @@
[:td (password-field :pwd)]]
[:tr
[:td]
- [:td [:button {:type "submit"} "Log In"]]]
- [:tr
- [:td]
- [:td
- [:a {:href "/login/reset"} "Forgot your password?"]]]]))
+ [:td [:button {:type "submit"} "Log In"]]]]))
+
+(def openid-login-box
+ (form-to [:post "/openid-login"]
+ [:table
+ [:tr
+ [:td (label :openid-url "OpenID URL")]
+ [:td (text-field "openid-url")]]
+ [:tr
+ [:td]
+ [:td [:button {:type "submit"} "Log In With OpenID"]]]
+ [:tr
+ [:td]
+ [:td
+ [:a {:href "/login/reset"} "Forgot your password?"]]]]))
(def-page my-login-page [location]
(do
(if location (session/session-put! :login-to location))
{:title "4clojure - login"
:content
(content-page
- {:main login-box})}))
+ {:main [:div login-box openid-login-box]})}))
(defn do-login [user pwd]
(let [user (.toLowerCase user)
{db-pwd :pwd} (from-mongo (fetch-one :users :where {:user user}))
location (session/session-get :login-to)]
(if (and db-pwd (.checkPassword (StrongPasswordEncryptor.) pwd db-pwd))
- (do (update! :users {:user user}
+ (let [{possible-openid :openid} (from-mongo (fetch-one :users :where {:user user} :only [:openid]))
+ merged-user {:user user :openid possible-openid}]
+ (update! :users {:user user}
{:$set {:last-login (java.util.Date.)}}
:upsert false) ; never create new users accidentally
- (session/session-put! :user user)
+ (session/session-put! :user merged-user)
(session/session-delete-key! :login-to)
(response/redirect (or location "/problems")))
(flash-error "/login" "Error logging in."))))
@@ -113,12 +129,53 @@
(flash-error "/login/reset"
(err-msg "security.err-pwd-email" name)))))
(flash-error "/login/reset"
- (err-msg "security.err-unknown"))))
+ (err-msg "security.err-unknown"))))
+
+(def-page openid-failure [r]
+ {:title "OpenID Failure"
+ :content (content-page {:main [:div [:p "The OpenID you provided could not be verified. Please go back and try again."]]})})
+
+(defn openid-success [r]
+ (let [claimed-id (-> r :params :openid.claimed_id)
+ user {:openid claimed-id}
+ session-user (session/session-get :user) ; non-nil if already logged in
+ session-user {:user (:user (get-user session-user))}
+ location (session/session-get :login-to)
+ merged-user (merge session-user user)]
+ (when (:user merged-user)
+ (update! :users {:user (:user merged-user)}
+ {:$set {:openid claimed-id}})
+ (destroy! :users {:user nil :openid claimed-id}))
+ (update! :users {:openid claimed-id}
+ {:$set {:last-login (java.util.Date.)}})
+ (let [db-user (fetch-one :users :where {:openid claimed-id} :only [:user :openid])]
+ (session/session-put! :user db-user))
+ (session/session-delete-key! :login-to)
+ (response/redirect (or location "/problems"))))
+
+;; Putting the session info that openid needs in the sandbar session
+;; doesn't work. Thus, I'll make a little hack around that.
+(def openid-sessions (atom {}))
+
+(defn do-openid-login [openid-url session-value]
+ (let [redir (openid/redirect openid-url {} openid-callback-url)
+ sess session-value]
+ (swap! openid-sessions #(assoc % sess (:session redir)))
+ (dissoc redir :session)))
+
+(defn do-openid-callback [r]
+ (if (openid/validate (assoc r :session (merge (get r :session) (get @openid-sessions (-> r :cookies (get "ring-session") :value)))))
+ (openid-success r)
+ (openid-failure r)))
(defroutes login-routes
(GET "/login" [location] (my-login-page location))
(POST "/login" {{:strs [user pwd]} :form-params}
- (do-login user pwd))
+ (do-login user pwd))
+ (POST "/openid-login" {{:strs [openid-url]} :form-params {:strs [ring-session]} :cookies}
+ (do-openid-login openid-url (:value ring-session)))
+ (GET "/openid-callback" [:as r]
+ (do-openid-callback r))
(GET "/login/reset" [] (reset-password-page))
(POST "/login/reset" [email]
@@ -5,7 +5,7 @@
[ring.util.response :as response]
[cheshire.core :as json])
(:import [org.apache.commons.mail EmailException])
- (:use [foreclojure.utils :only [from-mongo get-user get-solved login-link flash-msg flash-error row-class approver? can-submit? send-email image-builder if-user with-user as-int maybe-update escape-html]]
+ (:use [foreclojure.utils :only [from-mongo get-user get-solved login-link flash-msg flash-error row-class approver? can-submit? send-email image-builder if-user with-user as-int maybe-update escape-html user-id]]
[foreclojure.ring-utils :only [*url*]]
[foreclojure.template :only [def-page content-page]]
[foreclojure.social :only [tweet-link gist!]]
@@ -121,9 +121,7 @@
(maybe-update [old-score] dec)))))))))
(defn store-completed-state! [username problem-id code]
- (let [{user-id :_id} (fetch-one :users
- :where {:user username}
- :only [:_id])
+ (let [user-id (user-id username)
current-time (java.util.Date.)]
(when (not-any? #{problem-id} (get-solved username))
(update! :users {:_id user-id} {:$addToSet {:solved problem-id}
@@ -1,6 +1,7 @@
(ns foreclojure.settings
(:require [sandbar.stateful-session :as session]
- [ring.util.response :as response])
+ [ring.util.response :as response]
+ [foreclojure.login :as login])
(:import [org.jasypt.util.password StrongPasswordEncryptor])
(:use [hiccup.form-helpers :only [form-to label text-field password-field check-box]]
[foreclojure.utils :only [from-mongo flash-error flash-msg with-user form-row assuming send-email login-url plausible-email?]]
@@ -42,8 +43,14 @@
"Hide my solutions"]
[:br]))
+(defn assoc-openid-box [openid]
+ (list
+ [:p "Associate an OpenID with your account and you can log in with that in the future. Other settings will not change if this field is changed."]
+ (form-row
+ [text-field :openid "OpenID" openid])))
+
(def-page settings-page []
- (with-user [{:keys [user email] :as user-obj}]
+ (with-user [{:keys [user email openid] :as user-obj}]
{:title "Account settings"
:content
(content-page
@@ -59,49 +66,56 @@
[:h3 "Hide My Solutions"]
[:div#settings-follow (hide-settings-box user-obj)]
[:hr]
+ [:h3 "Associate an OpenID with your account"]
+ [:div#assoc-openid (assoc-openid-box openid)]
+ [:hr]
[:h3 "Profile Image"]
[:div (gravatar-img {:email email :size 64})]
[:p "To change your profile image, visit <a href='http://gravatar.com' target='_blank'>Gravatar</a> and edit the image for '" email "'."]
[:div#button-div
[:button {:type "submit"} "Submit"]]))})}))
-(defn do-update-settings! [new-username old-pwd new-pwd repeat-pwd email disable-codebox hide-solutions]
- (with-user [{:keys [user pwd]}]
- (let [encryptor (StrongPasswordEncryptor.)
- new-pwd-hash (.encryptPassword encryptor new-pwd)
- new-lower-user (.toLowerCase new-username)]
- (assuming [(or (= new-lower-user user) (nil? (fetch-one :users :where {:user new-lower-user})))
- (err-msg "settings.user-exists"),
- (< 3 (.length new-lower-user) 14)
- (err-msg "settings.uname-size"),
- (= new-lower-user
- (first (re-seq #"[A-Za-z0-9_]+" new-lower-user)))
- (err-msg "settings.uname-alphanum")
- (or (empty? new-pwd) (< 6 (.length new-pwd)))
- (err-msg "settings.npwd-size"),
- (= new-pwd repeat-pwd)
- (err-msg "settings.npwd-match")
- (or (empty? new-pwd)
- (.checkPassword encryptor old-pwd pwd))
- (err-msg "settings.pwd-incorrect")
- (plausible-email? email)
- (err-msg "settings.email-invalid")
- (nil? (fetch-one :users :where {:email email :user {:$ne user}}))
- (err-msg "settings.email-exists")]
- (do
- (update! :users {:user user}
- {:$set {:pwd (if (seq new-pwd) new-pwd-hash pwd)
- :user new-lower-user
- :email email
- :disable-code-box (boolean disable-codebox)
- :hide-solutions (boolean hide-solutions)}}
- :upsert false)
- (session/session-put! :user new-lower-user)
- (flash-msg "/problems"
- (str "Account for " new-lower-user " updated successfully")))
- (flash-error "/settings" why)))))
+(defn do-update-settings! [new-username old-pwd new-pwd repeat-pwd email disable-codebox hide-solutions new-openid cookie-val]
+ (with-user [{:keys [user pwd openid]}]
+ (if (not= openid new-openid)
+ (do
+ (session/session-put! :login-to "/settings")
+ (login/do-openid-login new-openid cookie-val))
+ (let [encryptor (StrongPasswordEncryptor.)
+ new-pwd-hash (.encryptPassword encryptor new-pwd)
+ new-lower-user (.toLowerCase new-username)]
+ (assuming [(or (= new-lower-user user) (nil? (fetch-one :users :where {:user new-lower-user})))
+ (err-msg "settings.user-exists"),
+ (< 3 (.length new-lower-user) 14)
+ (err-msg "settings.uname-size"),
+ (= new-lower-user
+ (first (re-seq #"[A-Za-z0-9_]+" new-lower-user)))
+ (err-msg "settings.uname-alphanum")
+ (or (empty? new-pwd) (< 6 (.length new-pwd)))
+ (err-msg "settings.npwd-size"),
+ (= new-pwd repeat-pwd)
+ (err-msg "settings.npwd-match")
+ (or (empty? new-pwd)
+ (.checkPassword encryptor old-pwd pwd))
+ (err-msg "settings.pwd-incorrect")
+ (plausible-email? email)
+ (err-msg "settings.email-invalid")
+ (nil? (fetch-one :users :where {:email email :user {:$ne user}}))
+ (err-msg "settings.email-exists")]
+ (do
+ (update! :users {:user user}
+ {:$set {:pwd (if (seq new-pwd) new-pwd-hash pwd)
+ :user new-lower-user
+ :email email
+ :disable-code-box (boolean disable-codebox)
+ :hide-solutions (boolean hide-solutions)}}
+ :upsert false)
+ (session/session-put! :user new-lower-user)
+ (flash-msg "/problems"
+ (str "Account for " new-lower-user " updated successfully")))
+ (flash-error "/settings" why))))))
(defroutes settings-routes
(GET "/settings" [] (settings-page))
- (POST "/settings" {{:strs [new-username old-pwd pwd repeat-pwd email disable-codebox hide-solutions]} :form-params}
- (do-update-settings! new-username old-pwd pwd repeat-pwd email disable-codebox hide-solutions)))
+ (POST "/settings" {{:strs [new-username old-pwd pwd repeat-pwd email disable-codebox hide-solutions openid]} :form-params {:strs [ring-session]} :cookies}
+ (do-update-settings! new-username old-pwd pwd repeat-pwd email disable-codebox hide-solutions openid (:value ring-session))))
@@ -3,14 +3,15 @@
(:use [hiccup.core :only [html]]
[hiccup.page-helpers :only [doctype javascript-tag link-to]]
[foreclojure.config :only [config repo-url]]
- [foreclojure.utils :only [page-attributes rendering-info login-url approver? can-submit?]]
+ [foreclojure.utils :only [page-attributes rendering-info login-url approver? can-submit? user-or-openid]]
[foreclojure.ring-utils :only [static-url]]
[foreclojure.version-utils :only [css js]]))
;; Global wrapping template
(defn html-doc [body]
(let [attrs (rendering-info (page-attributes body))
- user (session/session-get :user)]
+ user (session/session-get :user)
+ user (user-or-openid user)]
(html
(doctype :html5)
[:html
Oops, something went wrong.