diff --git a/HappyAPI.svg b/HappyAPI.svg index 6d829e9..1f4ad53 100644 --- a/HappyAPI.svg +++ b/HappyAPI.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/README.md b/README.md index efbd463..0a6e043 100644 --- a/README.md +++ b/README.md @@ -135,16 +135,18 @@ and then from a file `happyapi.edn`. When no port is specified (for example `:redirect_uri "http://localhost/redirect"`), HappyAPI listens on the default http port 80. Port 80 is a privileged port that requires root permissions, which may be problematic for some users. -Google allows the `redirect_uri` port to vary. +Google and GitHub allow the `redirect_uri` port to vary. Other providers do not. A random port is a natural choice. Configuring `:redirect_uri "http://localhost:0/redirect"` will listen on a random port. -This is the default used for Google if not configured otherwise. +This is the default used for Google and GitHub if not configured otherwise. You can choose a port if you'd like. If you want to listen on port 8080, configure `:redirect_uri "http://localhost:8080/redirect"` -You need to update your provider settings to match. -Most providers require an exact match between the provider side settings and client config, +This is the default used for Twitter if not configured otherwise. + +You must update your provider settings to match either the default, or your own `redirect_uri`. +Providers require an exact match between the provider side settings and client config, so please check this carefully if you get an error. ### Instrumentation, logging, and metrics diff --git a/deps.edn b/deps.edn index 1e4d263..c280993 100644 --- a/deps.edn +++ b/deps.edn @@ -1,4 +1,4 @@ -{:paths ["src"] +{:paths ["src" "resources"] :deps {org.clojure/clojure {:mvn/version "1.11.3"} buddy/buddy-sign {:mvn/version "3.5.351"} diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 120000 index 0000000..25b2b84 --- /dev/null +++ b/docs/favicon.ico @@ -0,0 +1 @@ +../resources/favicon.ico \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/resources/favicon.ico b/resources/favicon.ico new file mode 100644 index 0000000..538d0a9 Binary files /dev/null and b/resources/favicon.ico differ diff --git a/src/happyapi/middleware.clj b/src/happyapi/middleware.clj index 6f9cf49..d6b40fe 100644 --- a/src/happyapi/middleware.clj +++ b/src/happyapi/middleware.clj @@ -168,10 +168,17 @@ keywordize-keys (wrap-keywordize-keys))) ;; TODO: surely there are other cases to consider? +(defn remove-redundant-data-labels [x] + (if (map? x) + (cond (contains? x :data) (recur (get x :data)) + (contains? x "data") (recur (get x "data")) + (seq (get x :items)) (get x :items) + (seq (get x "items")) (get x "items") + :else x) + x)) + (defn extract-result [{:keys [body]}] - (cond (and (map? body) (seq (get body :items))) (get body :items) - (and (map? body) (seq (get body "items"))) (get body "items") - :else body)) + (remove-redundant-data-labels body)) (defn wrap-extract-result "When we call an API, we want the logical result of the call, not the map containing body, and status. diff --git a/src/happyapi/oauth2/capture_redirect.clj b/src/happyapi/oauth2/capture_redirect.clj index 0355310..191493e 100644 --- a/src/happyapi/oauth2/capture_redirect.clj +++ b/src/happyapi/oauth2/capture_redirect.clj @@ -3,10 +3,12 @@ If you are making a web app, implement a route in your app that captures the code parameter. If you use this namespace, add ring as a dependency in your project." (:require [clojure.java.browse :as browse] + [clojure.java.io :as io] [clojure.set :as set] [happyapi.middleware :as middleware] [happyapi.oauth2.auth :as oauth2] - [ring.middleware.params :as params])) + [ring.middleware.params :as params]) + (:import (java.io FileInputStream))) (set! *warn-on-reflection* true) @@ -18,6 +20,18 @@ (-> (oauth2/provider-login-url config scopes optional) (browse/browse-url))) +(defn make-redirect-handler [p] + (-> (fn redirect-handler [{:as req :keys [request-method uri params]}] + (case [request-method uri] + [:get "/favicon.ico"] {:body (io/file (io/resource "favicon.ico")) + :status 200} + (if (get @(deliver p params) "code") + {:status 200 + :body "Code received, authentication successful."} + {:status 400 + :body "No code in response."}))) + (params/wrap-params))) + (defn fresh-credentials "Opens a browser to authenticate, waits for a redirect, and returns a code. Defaults access_type to offline, @@ -39,15 +53,8 @@ port (if requested-port (Integer/parseInt requested-port) 80) - http-redirect-handler (fn [request] - (if (get @(deliver p (get request :params)) "code") - {:status 200 - :body "Code received, authentication successful."} - {:status 400 - :body "No code in response."})) - handler (params/wrap-params http-redirect-handler) {:keys [run-server]} fns - {:keys [port stop]} (run-server handler {:port port}) + {:keys [port stop]} (run-server (make-redirect-handler p) {:port port}) ;; The port may have changed when requesting a random port config (if requested-port (assoc config :redirect_uri (str protocol host ":" port path)) @@ -70,7 +77,7 @@ ;; wait for the user to get redirected to localhost with a code {:strs [code state] :as return-params} (deref p login-timeout nil)] ;; allow a bit of time to deliver the response before shutting down the server - (stop) + (Thread. (fn [] (Thread/sleep 1000) (stop))) (if code (do (when-not (= state state-and-challenge) diff --git a/src/happyapi/oauth2/client.clj b/src/happyapi/oauth2/client.clj index 886e556..06a255c 100644 --- a/src/happyapi/oauth2/client.clj +++ b/src/happyapi/oauth2/client.clj @@ -16,17 +16,20 @@ (defmethod endpoints :google [_] {:auth_uri "https://accounts.google.com/o/oauth2/auth" :token_uri "https://oauth2.googleapis.com/token" - ;; port 0 indicates random port + ;; port 0 selects a random port :redirect_uri "http://localhost:0/redirect" :authorization_options {:access_type "offline" :prompt "consent" :include_granted_scopes true}}) (defmethod endpoints :github [_] - {:auth_uri "https://github.com/login/oauth/authorize" - :token_uri "https://github.com/login/oauth/access_token"}) + {:auth_uri "https://github.com/login/oauth/authorize" + :token_uri "https://github.com/login/oauth/access_token" + ;; port 0 selects a random port + :redirect_uri "http://localhost:0/redirect"}) (defmethod endpoints :twitter [_] {:auth_uri "https://twitter.com/i/oauth2/authorize" :token_uri "https://api.twitter.com/2/oauth2/token" + :redirect_uri "http://localhost:8080/redirect" :authorization_options {:code_challenge_method "plain"}}) (defn with-endpoints diff --git a/test/happyapi/oauth2/capture_redirect_test.clj b/test/happyapi/oauth2/capture_redirect_test.clj index 411ea8d..fc9dd9f 100644 --- a/test/happyapi/oauth2/capture_redirect_test.clj +++ b/test/happyapi/oauth2/capture_redirect_test.clj @@ -15,19 +15,23 @@ {:auth_uri "TEST" :client_id "TEST" :redirect_uri "http://localhost"} - [] - {}))) + []))) (is (= {:access_token "TOKEN"} (capture-redirect/fresh-credentials http/request {:auth_uri "TEST" :client_id "TEST" :redirect_uri "http://localhost:8080/redirect"} - [] - {}))) + []))) (is (thrown? Throwable (capture-redirect/fresh-credentials http/request {:auth_uri "TEST" :client_id "TEST" :redirect_uri "http://not.localhost"} - [] - {}))))) + []))))) + +(deftest make-redirect-handler-test + (let [p (promise)] + + (capture-redirect/make-redirect-handler p) + () + )) diff --git a/test/happyapi/providers/twitter_test.clj b/test/happyapi/providers/twitter_test.clj index c9a0cdc..c277d0e 100644 --- a/test/happyapi/providers/twitter_test.clj +++ b/test/happyapi/providers/twitter_test.clj @@ -1,20 +1,19 @@ (ns happyapi.providers.twitter-test - (:require [clojure.edn :as edn] - [clojure.test :refer :all] + (:require [clojure.test :refer :all] [happyapi.providers.twitter :as twitter])) (deftest api-request-test - (twitter/setup! (assoc-in (edn/read-string (slurp "happyapi.edn")) - [:twitter :redirect_uri] - "http://localhost:8080/redirect")) - (twitter/api-request {:method :get - :url "https://api.twitter.com/2/users/me" - :scopes ["tweet.read" "tweet.write" "users.read"]}) - (twitter/api-request {:method :delete - :url "https://api.twitter.com/2/tweets/1811986925798195513" - :scopes ["tweet.read" "tweet.write" "users.read"]}) + (twitter/setup! nil) + (is (-> (twitter/api-request {:method :get + :url "https://api.twitter.com/2/users/me" + :scopes ["tweet.read" "tweet.write" "users.read"]}) + :username)) + (is (-> (twitter/api-request {:method :delete + :url "https://api.twitter.com/2/tweets/1811986925798195513" + :scopes ["tweet.read" "tweet.write" "users.read"]}) + :deleted)) ;; let's not post every time I run the tests... #_(twitter/api-request {:method :post - :url "https://api.twitter.com/2/tweets" - :scopes ["tweet.read" "tweet.write" "users.read"] - :body {:text "This is a test tweet from HappyAPI"}})) + :url "https://api.twitter.com/2/tweets" + :scopes ["tweet.read" "tweet.write" "users.read"] + :body {:text "This is a test tweet from HappyAPI"}}))