How do I pass in a custom 401 Not Authorized message when :not-authorized? is true? I'd like to show a more useful message like Key must have admin permissions.
You need to take a look at handlers:
For every http status code there is a handler function defined in liberator. All have sensible defaults and will return a simple english error message or an empty response, whatever is appropriate.
In your case you need to provide your own handler for :handle-unauthorized key:
(defresource my-resource
...
:handle-unauthorized (fn [ctx] ...))
Related
I am making a get and post requests like so:
(http-cljs.client/get "someurl.com/my")
and
(http-cljs.client/post "someurl.com/my")
On the server, I have the route:
{"/my" do-something}
While do-something runs with the get request, it doesn't with the post and in the client I get 403 forbidden. In the response I get "Invalid Anti-forgery token".
These are the middleware I'm using:
(defn config []
{:http-port (Integer. (or (env :port) 5000))
:middleware [[wrap-defaults site-defaults]
wrap-with-logger
wrap-gzip
[wrap-reload {:dir "../../src"}]
wrap-params
wrap-keyword-params
wrap-json-body
wrap-edn-params
]})
When I use api-defaults however, there's no 403 forbidden, and it only happens with the site-defaults. Why is this the case?
The configuration for wrap-defaults which is site-defaults will turn on the anti-forgery-middleware
If you look at the doc string of wrap-anti-forgery you will find:
"Middleware that prevents CSRF attacks. Any POST request to the handler
returned by this function must contain a valid anti-forgery token, or else an
access-denied response is returned.
The anti-forgery token can be placed into a HTML page via the
*anti-forgery-token* var, which is bound to a (possibly deferred) token.
The token is also available in the request under
`:anti-forgery-token`.
By default, the token is expected to be POSTed in a form field named
'__anti-forgery-token', or in the 'X-CSRF-Token' or 'X-XSRF-Token'
headers.
Accepts the following options:
:read-token - a function that takes a request and returns an anti-forgery
token, or nil if the token does not exist
:error-response - the response to return if the anti-forgery token is
incorrect or missing
:error-handler - a handler function to call if the anti-forgery token is
incorrect or missing
:strategy - a strategy for creating and validating anti-forgety tokens,
which must satisfy the
ring.middleware.anti-forgery.strategy/Strategy protocol
(defaults to the session strategy:
ring.middleware.anti-forgery.session/session-strategy)
Only one of :error-response, :error-handler may be specified.
Anti-forgery for forms is used to prevent replay attacks.
More info on CSRF attacks here: https://en.wikipedia.org/wiki/Cross-site_request_forgery
I'd like to get OpenID connect working in my little luminus project. I'm a little new to the workflow in luminus/ring/compojure (coming from django, flask, and servlets mostly). I've successfully redirected to Google so I get the "code" back from Google, but then I need to make one more request to Google before logging in the user and this call requires another callback the user is not involved in, so I need to put the user's request on hold like a promise, but I'm not sure how that part works in compojure.
; this is my code that redirects them to Google, where they accept
(defn login [params]
(let [google-oauth2-client-id (System/getenv "GOOGLE_OAUTH2_CLIENT_ID")
base-url "https://accounts.google.com/o/oauth2/auth"
args {"client_id" google-oauth2-client-id
"response_type" "code"
"scope" "openid email"
"redirect_uri" "http://localhost:3000/oauth2Callback"
"state" "anti-forgery here"}]
(assert google-oauth2-client-id "can't find GOOGLE_OAUTH2_CLIENT_ID in environment")
(redirect (str base-url "?" (make-query-string args)))
)
)
; this is my code handling Google's first response
(defn oauth2-callback [params]
; params has the code to send to Google
; here I should send another request to google that comes back to another callback like oauth2-token-callback that processes the request to the user in the current context
(redirect "/youreloggedin")
)
By the end of this method I should be sending the user a message saying they're logged in, but I need to wait until the request comes back. How is this workflow handled in luminus?
Solved. I didn't realize I could just ignore the callback parameter.
(client/post "https://www.googleapis.com/oauth2/v3/token"
{:headers {"X-Api-Version" "2"}
:content-type :application/x-www-form-urlencoded
:form-params {:code (params :code)
:client_id (System/getenv "GOOGLE_OAUTH2_CLIENT_ID")
:client_secret (System/getenv "GOOGLE_OAUTH2_CLIENT_SECRET")
:redirect_uri "http://localhost:3000/oauth2Callback" ; ignored
:grant_type "authorization_code"
}
:as :auto ; decode the body straight to hash (if possible)
})
Based on the documentation for Google's OAuth2 for web servers here, the flow consists of the following steps:
Your application redirects a browser to a Google URL; the URL includes query parameters that indicate the type of access being requested.
The result is an authorization code, which Google returns to your application in a query string.
After receiving the authorization code, your application can exchange the code (along with a client ID and client secret) for an access token and, in some cases, a refresh token.
If I understood your question correctly, step 3 does not necessarily involve a callback to your server, you can just perform the request to Google with an HTTP client. I recently implemented OAuth2 for GitHub in this project, step 3 is implemented in this function:
(defn callback
"Handles the callback from GitHub OAuth flow."
[code]
(let [params {:form-params {:client_id client-id
:client_secret client-secret
:code code}}
{:keys [body]} (client/post access-token-url params) ;; This is doing the POST
;; request to GitHub.
token ((qs-map body) "access_token")] ;; Getting the token from
;; the response here.
{:status 302
:headers {"location" "/repos"
"set-cookie" (str "token=" token ";Path=/")}}))
I used clj-http as the HTTP client but any other will do.
I'm using the ring basic-authentication library available for compojure. The authenticated? function takes a username and a password in order to authenticate, but in my particular case I need to access other parameters passed in by the user request besides a username and a password.
For instance, consider a case where there are multiple servers, and a user has an account on a particular server. That user would therefore need to authenticate with (log-on to) a particular server. Therefore I need his username, password, AND server to do the authentication.
Handling a "normal" case which just calls for a username and password might look something like this (using example sql to hit a database):
; curl my_id:my_pass#localhost/my_request
(defn authenticated? [id password]
(first (select user (where {:id id :password password}) (limit 1))))
I'd like to do something like this:
; curl my_id:my_pass#localhost/my_server/my_request
(defn authenticated? [id password ??server??]
(first (select user (where {:server server :id id :password password}) (limit 1))))
I guess my question is, how do I access all request params from inside authenticated? Or, alternatively, how do I pass the server id into the authenticated? function?
Thanks.
In accordance with the comments to the question above, the approach would look like this (just a sketch, I haven't tested whether this works due to lack of a running setup with ring):
(defn wrap-basic-auth-server [handler authfn]
(fn [request]
(-> request
(wrap-basic-authentication (partial authfn (get-in request [:params :server])))
handler)))
What's happening here is that the code is assuming that your server url param will have been added by wrap-params (from ring.middleware.params) to the request map. wrap-basic-authentication is called then with the handler (typical ring middleware, i.e. any other wrapper / handler coming afterwards) and a new (partial) function, which is just your authenticate function which has already swallowed the server arg.
And then instead of just calling wrap-basic-authentication in your routes, you need to add wrap-params and wrap-basic-auth-server and you need your new auth function. E.g.
(defn authenticated? [server user pass]
... your code ...)
(def app
(-> app-routes
... potential other wrappers elided ...
(wrap-params)
(wrap-basic-auth-server authenticated?)))
As I said, I've not tested this code, but it should get you started.
When I click the search button on this page, it sends a post request. I want to do the post via cli-http. How can I do that?
(def default-http-opts
{:socket-timeout 10000
:conn-timeout 10000
:insecure? true
:throw-entire-message? false})
(clj-http/post initial-url default-http-opts)
can post a request but the problem is that I want to pass in some parameters. These parameters(the buttons selected) are default on the page.
They are:
AdvancedSearchForm:CourseOrSubjectSelection=ALL_ALL
AdvancedSearchForm:GraduateLevelSelection=ALL
AdvancedSearchForm:allStudyAreas=t
AdvancedSearchForm:departmentList=
AdvancedSearchForm:facultyList=
AdvancedSearchForm:keywords=
AdvancedSearchForm:level=ALL
AdvancedSearchForm:semester=ALL
oracle.adf.faces.FORM=AdvancedSearchForm
oracle.adf.faces.STATE_TOKEN=_id21519:_id21520
source=AdvancedSearchForm:searchButton
The key AdvancedSearchForm:semester contains ':', so I use string as a key like this "AdvancedSearchForm:semester", is it OK in clj-http?
I do it like this:
(spit (file "/tmp" "ts.html")
(:body (http/post initial-url
{:form-params {"AdvancedSearchForm:CourseOrSubjectSelection" "ALL_ALL", "AdvancedSearchForm:GraduateLevelSelection" "ALL"}})))`
Actually the page it returns is indeed "Results" but no courses are listed. only the template. I want to get all the course links which are only shown by manually click. Any help?
is the image I screenshot from Tamper Data. It shows what happens after I click the Search button. Seems like client is redirected to searchresult.jsp. I use curl to imitate that. I do it like this
curl -D "form data..." https://handbook.unimelb.edu.au/faces/htdocs/user/search/AdvancedSearch.jsp
Then quickly run
curl https://handbook.unimelb.edu.au/faces/htdocs/user/search/SearchResults.jsp
No results contents are shown though the page is downloaded.
It looks like the server doesn't understand the parameters you send to it.
The escaping in use is the percent-encoding. Try to check out if it get in use by using the debug functionality avail in clj-https README.md:
;; print request info to *out*, including request body:
(client/post "http://example.org" {:debug true :debug-body true :body "..."})
or try to manually run the requests either with the curl command in a terminal or with the convenient Firefox restclient add-on.
From their GitHub page (https://github.com/dakrone/clj-http):
;; Send form params as a urlencoded body (POST or PUT)
(client/post "http//site.com" {:form-params {:foo "bar"}})
I am using clojure liberator to expose my services as REST service,I have a POST request, Below is the code, I could do the process on calling the service as POST, but I want to send back the event id as response of the POST, Can anyone help
(defresource send-event-resource
:method-allowed? (request-method-in :post)
:available-media-types ["text/plain"]
:post! (fn [context]
(workers/send-event context)))
Raised an issue in liberator, got the response from there
https://github.com/clojure-liberator/liberator/issues/61
Put the event id in to context map and look it up from handle-ok:
(defresource send-event-resource
:method-allowed? (request-method-in :post)
:available-media-types ["text/plain"]
:post! (fn [context] {::event-id (workers/send-event context)})
:handle-ok ::event-id)
The above code makes use of the fact that a clojure keyword is a function that when applied to the context map will look up "itself".